index.js

'use strict';

/**
 * The default module of the rekit-core package. It does two things:
 *
 * 1. Wraps modularized APIs to Rekit core commands.
 * 2. Exports other modules. i.e. `require('rekit-core').component` equals to `require('rekit-core/component')`.
 *
 * @module rekit-core
**/

/**
 * The combination of feature and name of an element like component.
 * @typedef {Object} ElementArg
 * @property {string} feature - The feature of the element.
 * @property {string} name - The name of the element.
 * @alias module:rekit-core.ElementArg
 */

const _ = require('lodash');

/**
 * Quick access to rekit-core/app.
 * @alias module:rekit-core.app
**/
const app = require('./app');

/**
 * Quick access to rekit-core/component.
 * @alias module:rekit-core.component
**/
const component = require('./component');

/**
 * Quick access to rekit-core/style.
 * @alias module:rekit-core.style
**/
const style = require('./style');

/**
 * Quick access to rekit-core/test.
 * @alias module:rekit-core.test
**/
const test = require('./test');

/**
 * Quick access to rekit-core/action.
 * @alias module:rekit-core.action
**/
const action = require('./action');

/**
 * Quick access to rekit-core/feature.
 * @alias module:rekit-core.feature
**/
const featureMgr = require('./feature');

/**
 * Quick access to rekit-core/utils.
 * @alias module:rekit-core.utils
**/
const utils = require('./utils');

/**
 * Quick access to rekit-core/vio.
 * @alias module:rekit-core.vio
**/
const vio = require('./vio');

/**
 * Quick access to rekit-core/vio.
 * @alias module:rekit-core.vio
**/
const refactor = require('./refactor');
const entry = require('./entry');
const route = require('./route');
const template = require('./template');
const plugin = require('./plugin');
const constant = require('./constant');

const injectExtensionPoints = plugin.injectExtensionPoints;

/**
 * Add a component including unit test and style files. It wraps APIs from `component`, `style` and `test`.
 *
 * @param {string} feature - the feature where to create the component.
 * @param {string} name - the component name, will be converted to pascal case.
 * @param {object} args - other args.
 * @alias module:rekit-core.addComponent
 *
 * @example <caption>Create a container component</caption>
 * const rekitCore = require('rekit-core');
 *
 * // Add a component named 'TopicList' which is connected to Redux store.
 * rekitCore.addComponent('home', 'topic-list', { connect: true });
 *
 * // Write the changes to disk. Otherwise only in memory, see more at rekitCore/vio
 * rekitCore.vio.flush();
**/
function addComponent(feature, name, args) {
  feature = _.kebabCase(feature);
  name = _.pascalCase(name);

  args = args || {};
  component.add(feature, name, {
    templateFile: args.connect ? 'ConnectedComponent.js' : 'Component.js',
  });
  if (args.urlPath) {
    let urlPath = args.urlPath;
    if (urlPath === '$auto') urlPath = name;
    urlPath = _.kebabCase(urlPath);
    route.add(feature, name, { urlPath, isIndex: !!args.isIndex });
  }
  style.add(feature, name);
  test.add(feature, name, {
    templateFile: args.connect ? 'ConnectedComponent.test.js' : 'Component.test.js',
  });
}

/**
 * Remove a component including unit test and style files. It wraps APIs from `component`, `style` and `test`.
 *
 * @param {string} feature - the feature where to create the component.
 * @param {string} name - the component name, will be converted to pascal case.
 * @alias module:rekit-core.removeComponent
 *
 * @example <caption>Remove a container component</caption>
 * const rekitCore = require('rekit-core');
 *
 * // Remove a component named 'TopicList' which is connected to Redux store.
 * rekitCore.removeComponent('home', 'topic-list');
 *
 * // Write the changes to disk. Otherwise only in memory, see more at rekitCore/vio
 * rekitCore.vio.flush();
**/
function removeComponent(feature, name) {
  feature = _.kebabCase(feature);
  name = _.pascalCase(name);
  component.remove(feature, name);
  route.remove(feature, name);
  style.remove(feature, name);
  test.remove(feature, name);
}

/**
 * Move/rename a component including unit test and style files. It wraps APIs from `component`, `style` and `test`.
 *
 * @param {object} source - which component to be moved, in form of { name: {string}, feature: {string} }.
 * @param {object} target - where to move, in form of { name: {string}, feature: {string} }.
 * @alias module:rekit-core.moveComponent
 *
 * @example <caption>Rename TopicList to Topics</caption>
 * const rekitCore = require('rekit-core');
 *
 * // Rename the component from 'TopicList' to 'Topics'
 * rekitCore.moveComponent({ feautre: 'home', name: 'topic-list' }, { feautre: 'home', name: 'topics' });
 *
 * // Write the changes to disk. Otherwise only in memory, see more at rekitCore/vio
 * rekitCore.vio.flush();
**/
function moveComponent(source, target) {
  source.feature = _.kebabCase(source.feature);
  source.name = _.pascalCase(source.name);
  target.feature = _.kebabCase(target.feature);
  target.name = _.pascalCase(target.name);

  component.move(source, target);
  test.move(source, target);
  style.move(source, target);
  route.move(source, target);
}

/**
 * Add an async action using redux-thunk and its unit test.
 *
 * @param {string} feature - the feature where to create the action.
 * @param {string} name - the action name, will be converted to camel case.
 * @alias module:rekit-core.addAsyncAction
 *
**/
function addAsyncAction(feature, name) {
  action.addAsync(feature, name);
  test.addAction(feature, name, { isAsync: true });
}

/**
 * Remove an async action and its unit test.
 *
 * @param {string} feature - the feature of the action.
 * @param {string} name - the action name, will be converted to camel case.
 * @alias module:rekit-core.removeAsyncAction
 *
**/
function removeAsyncAction(feature, name) {
  action.removeAsync(feature, name);
  test.removeAction(feature, name);
}

/**
 * Move/rename an async action including unit test.
 *
 * @param {ElementArg} source - Which action to be moved.
 * @param {ElementArg} target - Where to move.
 * @alias module:rekit-core.moveAsyncAction
 *
**/
function moveAsyncAction(source, target) {
  action.moveAsync(source, target);
  test.moveAction(source, target, { isAsync: true });
}

/**
 * Add an action and its unit test.
 *
 * @param {string} feature - the feature where to create the action.
 * @param {string} name - the action name, will be converted to camel case.
 * @alias module:rekit-core.addAction
 *
**/
function addAction(feature, name, args) {
  args = args || {};
  if (args.async) {
    addAsyncAction(feature, name);
    return;
  }
  action.add(feature, name);
  test.addAction(feature, name);
}

/**
 * Remove an action and its unit test.
 *
 * @param {string} feature - the feature of the action.
 * @param {string} name - the action name, will be converted to camel case.
 * @alias module:rekit-core.removeAction
 *
**/
function removeAction(feature, name) {
  const targetPath = utils.mapReduxFile(feature, name);
  if (_.get(app.getRekitProps(targetPath), 'action.isAsync')) {
    removeAsyncAction(feature, name);
    return;
  }
  action.remove(feature, name);
  test.removeAction(feature, name);
}

/**
 * Move/rename an action including unit test.
 *
 * @param {object} source - which action to be moved, in form of { name: {string}, feature: {string} }.
 * @param {object} target - where to move, in form of { name: {string}, feature: {string} }.
 * @alias module:rekit-core.moveAction
 *
**/
function moveAction(source, target) {
  const targetPath = utils.mapReduxFile(source.feature, source.name);
  if (_.get(app.getRekitProps(targetPath), 'action.isAsync')) {
    moveAsyncAction(source, target);
    return;
  }
  action.move(source, target);
  test.moveAction(source, target);
}

/**
 * Add a feature with one sample component. Besides creating feature folder and files,
 * it also registers reducer, router config, style to root config of the app.
 *
 * @param {string} name - the feature name.
 * @alias module:rekit-core.addFeature
 *
**/
function addFeature(name) {
  featureMgr.add(name);
  entry.addToRootReducer(name);
  entry.addToRouteConfig(name);
  entry.addToRootStyle(name);

  // Add default page and sample action
  addComponent(name, 'default-page', { isIndex: true, connect: true, urlPath: '$auto' });
}

/**
 * Remove a feature.
 *
 * @param {string} name - the feature name.
 * @alias module:rekit-core.removeFeature
 *
**/
function removeFeature(name) {
  featureMgr.remove(name);
  entry.removeFromRootReducer(name);
  entry.removeFromRouteConfig(name);
  entry.removeFromRootStyle(name);
}

/**
 * Rename a feature
 *
 * @param {string} oldName - Old name of the feature.
 * @param {string} newName - New name of the feature.
 * @alias module:rekit-core.moveFeature
 *
**/
function moveFeature(oldName, newName) {
  featureMgr.move(oldName, newName);
}

const coreCommands = {
  addComponent: injectExtensionPoints(addComponent, 'add', 'component'),
  removeComponent: injectExtensionPoints(removeComponent, 'remove', 'component'),
  moveComponent: injectExtensionPoints(moveComponent, 'move', 'component'),
  addAction: injectExtensionPoints(addAction, 'add', 'action'),
  removeAction: injectExtensionPoints(removeAction, 'remove', 'action'),
  moveAction: injectExtensionPoints(moveAction, 'move', 'action'),
  addAsyncAction: injectExtensionPoints(addAsyncAction, 'add', 'async-action'),
  removeAsyncAction: injectExtensionPoints(removeAsyncAction, 'remove', 'async-action'),
  moveAsyncAction: injectExtensionPoints(moveAsyncAction, 'move', 'async-action'),
  addFeature: injectExtensionPoints(addFeature, 'add', 'feature'),
  removeFeature: injectExtensionPoints(removeFeature, 'remove', 'feature'),
  moveFeature: injectExtensionPoints(moveFeature, 'move', 'feature'),
};

function splitName(name) {
  const arr = name.split('/');
  return {
    feature: arr[0],
    name: arr[1],
  };
}

/**
 * Handle the parse result of Rekit command, like `rekit add component home/hello`.
 * @alias module:rekit-core.handleCommand
**/
function handleCommand(args) {
  const params = [];
  switch (args.commandName) {
    case 'add':
    case 'remove': {
      if (args.type === 'feature') params.push(args.name);
      else {
        params.push(splitName(args.name).feature);
        params.push(splitName(args.name).name);
      }
      break;
    }
    case 'move': {
      if (args.type === 'feature') {
        params.push(args.source);
        params.push(args.target);
      } else {
        params.push(splitName(args.source));
        params.push(splitName(args.target));
      }
      break;
    }
    default:
      break;
  }
  params.push(args);

  let cmd = plugin.getCommand(args.commandName, args.type);
  if (!cmd) {
    cmd = coreCommands[_.camelCase(args.commandName + '-' + args.type)];
  }

  if (!cmd) {
    utils.fatalError(`Can't find the desired command: ${args.commandName}`);
  }
  cmd.apply(null, params);
}

module.exports = Object.assign({
  app,
  vio,
  refactor,
  utils,
  component,
  constant,
  style,
  test,
  action,
  template,
  feature: featureMgr,
  entry,
  route,
  plugin,

  handleCommand,
}, coreCommands);

// NOTE: plugin.loadPlutins should be executed after module.exports to avoid circular dependency
// plugin.loadPlugins();