Rekit 2.0 introduced a new plugin mechanism for extending the capability of Rekit.
If you've tried Rekit command line tools, you may have been familiar with its pattern:
rekit <add|rm|mv> <element-type> <feature>/</element-name>
Internally Rekit supports 3 element types: feature
, component
and action
and defines how to add/rm/mv
them.
Now you can create a Rekit plugin to alter the default behavior like how Rekit creates an async action or let Rekit support a new element type like selector
based on reselect.
Actually, there have been such two plugins available:
- rekit-plugin-redux-saga: allows Rekit to uses
redux-saga
rather thanredux-thunk
when creating async actions. - rekit-plugin-selector: adds a new element type named
selector
based on reselect to Rekit. So that you can manage selectors by Rekit for your project.
The two plugins are not only productable but also reference about how to create Rekit plugins.
There are two types of plugins:
- Public plugins: plugins published on npm so that all people could use it. When installed, it's under
node_modules
folder. In development time, you can use npm link to test a public plugin. - Local plugins: plugins only meet the requirements for your own project. They are placed under
<your-prjroot>/tools/plugins
folder.
There are no difference of the two types of plugins themselves.
To create a plugin, use below command:
rekit create-plugin <plugin-name>
It will create a local plugin if the current directory is in a Rekit project, otherwise create a public plugin.
After creating a plugin, you can look into the folder structure. There could be some special files under the plugin folder:
The only mandotory file of a plugin, it defines element types it could handle and defines the command arguments if necessary. For example:
module.exports = {
accept: ['action'],
defineArgs(addCmd, mvCmd, rmCmd) { // eslint-disable-line
addCmd.addArgument(['--thunk'], {
help: 'Use redux-thunk for async actions.',
action: 'storeTrue',
});
},
};
There are two parts:
It's an array of element types that the plugin could handle. Here it's action
, it will override the default behavior how Rekit handles action
. Whenever an element type is defined here, there should be a module named ${elementType}.js
where defines add
, remove
, move
method to manage those elements. For example, element type action
is defined, there should be a module action.js
in the plugin folder:
module.exports = {
add(feature, name, args) {},
remove(feature, name, args) {},
move(source, target, args) {},
};
You can export just some of add
, remove
, move
if needed. For example, if you only defines add
command, then when executes rekit mv action ...
it will fallback to the default behavior of how Rekit mv
actions.
You can also create 3 plugins handle add
, remove
, move
separately, they accept the same element type. Though seems useless.
Rekit uses argparse to parse command arguments. This methods allows to define custom arguments for command line tools. Here addCmd
, mvCmd
and rmCmd
are all sub-commands of the global rekit
command. According to the docs of argparse, you can add more options to sub-commands to meet your requirements. For example: redux-saga
plugin defines a new option --thunk
that allows to use redux-thunk
for async actions while redux-saga is used by default. Then you could use:
rekit add action home/my-action -a --thunk
This file is only necessary when you want to hook to some operations. i.e., do something after a feature is created or removed. Each element type has two kind of hook points: before
and after
, they are combined with operation types to form multiple hook points. For example, feature
element type has below hook points:
- beforeAddFeature()
- afterAddFeature()
- beforeMoveFeature()
- afterMoveFeature()
- beforeRemoveFeature()
- afterRemoveFeature()
The arguments just inherit from the hook target. That is, whatever arguments passed to addFeature are also passed to beforeAddFeature.
Note that not only internal element types have hook points, each element type supported by plugins all have such hook points.
For example, redux-saga
plugins uses hooks to do initialization and uninitialization when adding or removing features.
// Summary:
// This plugin allows to use redux-saga for redux async actions rather than
// redux-thunk by default. It overrides the action management provided by rekit-core.
// And it delegates action mgmt to rekit-core if it's sync.
const fs = require('fs');
const path = require('path');
const _ = require('lodash');
_.pascalCase = _.flow(_.camelCase, _.upperFirst);
_.upperSnakeCase = _.flow(_.snakeCase, _.toUpper);
module.exports = function(rekitCore) {
const app = rekitCore.app;
const utils = rekitCore.utils;
const refactor = rekitCore.refactor;
const test = rekitCore.test;
const action = rekitCore.action;
const vio = rekitCore.vio;
const afterAddFeature = require('./hooks')(rekitCore).afterAddFeature;
function ensureInit() {
// Summary
// Init the project to be ready for redux-saga
const rootSagaPath = utils.mapSrcFile('common/rootSaga.js');
// Check if saga plugin is already installed
if (fs.existsSync(rootSagaPath)) {
return;
}
// Create src/common/rootSaga.js
const content = vio.getContent(path.join(__dirname, 'templates/rootSaga.js'));
vio.save(rootSagaPath, content);
// Apply redux-saga middleware
const configStorePath = utils.mapSrcFile('common/configStore.js');
// Import saga modulesinit
refactor.updateFile(configStorePath, ast => [].concat(
refactor.addImportFrom(ast, 'redux-saga', 'createSagaMiddleware'),
refactor.addImportFrom(ast, './rootSaga', 'rootSaga'),
refactor.addToArray(ast, 'middlewares', 'sagaMiddleware')
));
// Add const sagaMiddleWare = createSagaMiddleware();
const lines = vio.getLines(configStorePath);
let i = refactor.lastLineIndex(lines, /^import/);
lines.splice(i + 1, 0, '', 'const sagaMiddleware = createSagaMiddleware();');
// Add sagaMiddleWare.run(rootSaga);
i = refactor.lineIndex(lines, 'return store;');
lines.splice(i, 0, ' sagaMiddleware.run(rootSaga);');
// Init existing features
console.log(app.getFeatures().forEach);
app.getFeatures().forEach(afterAddFeature);
vio.save(configStorePath, lines);
}
function add(feature, name, args) {
ensureInit();
feature = _.kebabCase(feature);
name = _.camelCase(name);
args = args || {};
if (!args.async || args.thunk) {
// Use default behavior if it's a sync action or asyn action with thunk by default.
rekitCore.addAction(feature, name, args);
return;
}
// Saga action is similar with async action except the template.
action.addAsync(feature, name, {
templateFile: path.join(__dirname, 'templates/async_action_saga.js'),
});
// Add to redux/sagas.js
const sagasEntry = utils.mapFeatureFile(feature, 'redux/sagas.js');
refactor.addExportFrom(sagasEntry, `./${name}`, null, `watch${_.pascalCase(name)}`);
// Add saga test
test.addAction(feature, name, {
templateFile: path.join(__dirname, 'templates/async_action_saga.test.js'),
isAsync: true,
});
}
function remove(feature, name) {
ensureInit();
feature = _.kebabCase(feature);
name = _.camelCase(name);
// Saga action is similar with default async action except the template.
rekitCore.removeAction(feature, name);
// Remove from sagas.js
const sagasEntry = utils.mapFeatureFile(feature, 'redux/sagas.js');
refactor.removeImportBySource(sagasEntry, `./${name}`);
}
function move(source, target) {
ensureInit();
rekitCore.moveAction(source, target);
source.feature = _.kebabCase(source.feature);
source.name = _.camelCase(source.name);
target.feature = _.kebabCase(target.feature);
target.name = _.camelCase(target.name);
let targetPath;
// rename saga function name
targetPath = utils.mapFeatureFile(target.feature, `redux/${target.name}.js`);
refactor.updateFile(targetPath, ast => [].concat(
refactor.renameFunctionName(ast, `do${_.pascalCase(source.name)}`, `do${_.pascalCase(target.name)}`),
refactor.renameFunctionName(ast, `watch${_.pascalCase(source.name)}`, `watch${_.pascalCase(target.name)}`)
));
// rename saga function name in test.js
targetPath = utils.mapTestFile(target.feature, `redux/${target.name}.test.js`);
refactor.renameImportSpecifier(targetPath, `do${_.pascalCase(source.name)}`, `do${_.pascalCase(target.name)}`);
if (source.feature === target.feature) {
targetPath = utils.mapFeatureFile(target.feature, 'redux/sagas.js');
refactor.updateFile(targetPath, ast => [].concat(
refactor.renameExportSpecifier(ast, `watch${_.pascalCase(source.name)}`, `watch${_.pascalCase(target.name)}`),
refactor.renameModuleSource(ast, `./${source.name}`, `./${target.name}`)
));
} else {
targetPath = utils.mapFeatureFile(source.feature, 'redux/sagas.js');
refactor.removeImportBySource(targetPath, `./${source.name}`);
targetPath = utils.mapFeatureFile(target.feature, 'redux/sagas.js');
refactor.addExportFrom(targetPath, `./${target.name}`, null, `watch${_.pascalCase(target.name)}`);
}
}
return {
add,
remove,
move,
};
}
For local plugins, you don't need to do anything else besides creating it. And it has highest priority for handling element types if there are conflicts.
For public plugins, that is installed from npm. You need to register it in rekit
section of package.json
:
{
...,
"rekit": {
"plugins": ["redux-saga", "apollo"],
},
...,
}
Here you should only define public plugins in the plugins
property so that they are loaded by Rekit. Local plugins will be always loaded by Rekit. Note that the order of plugin names matters while they accept same element types, the eariers have higher priority.
While there are conflicts that multiple plugins accept the same element type, the priority from high to low is: local plugins < public plugins < Rekit default behavior.
NOTE: plugins which support more element types could only be used via command line tools but not Rekit Studio for now.
For most cases, a plugin just creates boilerplate code based on some template; refactors code when moving, deleting source files. Rekit-core exports many useful APIs for accelerating plugin development. You may just need to compose those APIs to meet your requirement.
See the link: http://rekit.js.org/api/index.html