utils.js

/* eslint no-console: 0 */

'use strict';

/**
 * The common utility module. It provides various tools for managing a Rekit app.
 * @module
**/

const path = require('path');
const _ = require('lodash');
const shell = require('shelljs');
const colors = require('colors/safe');

let silent = false;

// NOTE: utils is just assumed to always be loaded before lodash is used...
_.pascalCase = _.flow(_.camelCase, _.upperFirst);
_.upperSnakeCase = _.flow(_.snakeCase, _.toUpper);

/**
 * Don't output any logs.
 * @param {boolean} isSilent - Whether to output logs.

 * @alias module:utils.setSilent
 *
**/
function setSilent(isSilent) {
  silent = isSilent;
}

/**
 * The unified version of 'path.join'. It forces forward slash ('/') on both unix like or windows system.
 * It also normalizes the path to emmit double slash and '..'.

 * @alias module:utils.joinPath
 *
 * @example
 * const utils = require('rekit-core').utils;
 * const p1 = utils.joinPath('c:\\abc', '../def');
 * // p1 => 'c:/def'
**/
function joinPath() {
  // A consistent and normalized version of path.join cross platforms
  return path.normalize(path.join.apply(path, arguments)).replace(/\\/g, '/');
}

/**
 * Log a message to console. It respects the setSilent switch.
 * @param {string} msg - The message to log.
**/
function log(msg) {
  if (!silent) console.log(msg);
}

/**
 * Log a warning message to console. It respects the setSilent switch.
 * @param {string} msg - The message to log.
**/
function warn(msg) {
  if (!silent) console.log(colors.yellow('Warning: ' + msg));
}

/**
 * Log an error message to console. It respects the setSilent switch.
 * @param {string} msg - The message to log.
**/
function error(msg) {
  if (!silent) console.log(colors.red('Error: ' + msg));
}

/**
 * Throw a fatal error and log an error message.
 * @param {string} msg - The message to log.
**/
function fatalError(msg) {
  error(msg);
  throw new Error(msg);
}

let prjRoot;

/**
 * By default Rekit will try to find the current Rekit project root. But you can also manually set it by calling this method.
 * @param {string} root - The project root.
**/
function setProjectRoot(root) {
  prjRoot = /\/$/.test(root) ? root : (root + '/');
  prjRoot = joinPath(prjRoot);
}

/**
 * Get the project root. By default it finds the Rekit project root of which the command is run.
**/
function getProjectRoot() {
  if (!prjRoot) {
    let cwd = process.cwd();
    let lastDir = null;
    // Traverse above until find the package.json.
    while (cwd && lastDir !== cwd) {
      const pkgPath = joinPath(cwd, 'package.json');
      if (shell.test('-e', pkgPath) && require(pkgPath).rekit) { // eslint-disable-line
        prjRoot = cwd;
        break;
      }
      lastDir = cwd;
      cwd = joinPath(cwd, '..');
    }
  }
  return joinPath(/\/$/.test(prjRoot) ? prjRoot : (prjRoot + '/'));
}

let pkgJson = null;
/**
 * Get the current project's package.json.
**/
function getPkgJson() {
  // Get the project package json
  if (!pkgJson) {
    const pkgJsonPath = joinPath(getProjectRoot(), 'package.json');
    pkgJson = require(pkgJsonPath);
  }
  return pkgJson;
}

/**
 * Set the current project's package.json.
**/
function setPkgJson(obj) {
  // Only used for unit test purpose
  pkgJson = obj;
}

/**
 * Get the relative path to the project root by given full path.
 * @param {string} fullPath - A full path string.
**/
function getRelativePath(fullPath) {
  // Get rel path relative to project root.
  const _prjRoot = getProjectRoot();
  const regExp = new RegExp(`^${_.escapeRegExp(_prjRoot)}`, 'i');
  return fullPath.replace(regExp, '');
}

/**
 * Get the full path of a relative path to the project root.
 * @param {string} relPath - The relative path.
**/
function getFullPath(relPath) {
  const _prjRoot = getProjectRoot();
  const regExp = new RegExp(`^${_.escapeRegExp(_prjRoot)}`, 'i');
  return regExp.test(relPath) ? relPath : joinPath(_prjRoot, relPath);
}

/**
 * Get action type constant for a sync action. It uses UPPER_SNAKE_CASE and combines feature name and action name.
 * @param {string} feature - The feature name of the action.
 * @param {string} action - The action name.
 *
 * @example
 * const utils = require('rekit-core').utils;
 * utils.getActionType('home', 'doSomething');
 * // => HOME_DO_SOMETHING
**/
function getActionType(feature, action) {
  return `${_.upperSnakeCase(feature)}_${_.upperSnakeCase(action)}`;
}

/**
 * Get action type constants for an async action. It uses UPPER_SNAKE_CASE and combines feature name and action name.
 * @param {string} feature - The feature name of the action.
 * @param {string} action - The action name.
 *
 * @example
 * const utils = require('rekit-core').utils;
 * utils.getAsyncActionTypes('home', 'doAsync');
 * // =>
 * // {
 * //   doAsyncBegin: 'HOME_DO_ASYNC_BEGIN',
 * //   doAsyncSuccess: 'HOME_DO_ASYNC_SUCCESS',
 * //   doAsyncFailure: 'HOME_DO_ASYNC_FAILURE',
 * //   doAsyncDismissError: 'HOME_DO_ASYNC_DISMISS_ERROR',
 * // }
**/
function getAsyncActionTypes(feature, action) {
  return {
    normal: getActionType(feature, action),
    begin: `${_.upperSnakeCase(feature)}_${_.upperSnakeCase(action)}_BEGIN`,
    success: `${_.upperSnakeCase(feature)}_${_.upperSnakeCase(action)}_SUCCESS`,
    failure: `${_.upperSnakeCase(feature)}_${_.upperSnakeCase(action)}_FAILURE`,
    dismissError: `${_.upperSnakeCase(feature)}_${_.upperSnakeCase(action)}_DISMISS_ERROR`,
  };
}

/**
 * Get the full path by a relative path to 'src'.
 * @param {string} filePath - The file path relative to project's 'src' folder.
 * @alias module:utils.mapSrcFile
 *
 * @example
 * const utils =require('rekit-core').utils;
 * utils.mapSrcFile('common/configStore.js');
 * // => /path/to/project/src/common/configStore.js
**/
function mapSrcFile(filePath) {
  return joinPath(getProjectRoot(), 'src', filePath);
}

function mapFeatureFile(feature, fileName) {
  return joinPath(getProjectRoot(), 'src/features', _.kebabCase(feature), fileName);
}

function mapTestFile(feature, fileName) {
  return joinPath(getProjectRoot(), 'tests/features', _.kebabCase(feature), fileName);
}

function mapComponent(feature, name) {
  // Map a component, page name to the file.
  return mapFeatureFile(feature, _.pascalCase(name));
}

function mapReduxFile(feature, name) {
  return mapFeatureFile(feature, 'redux/' + _.camelCase(name) + '.js');
}

function mapReduxTestFile(feature, name) {
  return mapTestFile(feature, 'redux/' + _.camelCase(name) + '.test.js');
}

function mapComponentTestFile(feature, name) {
  return mapTestFile(feature, _.pascalCase(name) + '.test.js');
}

function getFeatures() {
  return _.toArray(shell.ls(joinPath(getProjectRoot(), 'src/features')));
}

function getCssExt() {
  const pkgPath = joinPath(getProjectRoot(), 'package.json');
  const pkg = require(pkgPath); // eslint-disable-line
  return (pkg && pkg.rekit && pkg.rekit.css === 'sass') ? 'scss' : 'less';
}

function getFeatureName(filePath) {
  const relPath = getRelativePath(filePath);
  let name = null;

  if (_.startsWith(relPath, 'src/features')) {
    name = relPath.split('/')[2];
  }
  return name;
}

module.exports = {
  getCssExt,
  setProjectRoot,
  getProjectRoot,
  getPkgJson,
  setPkgJson,
  getRelativePath,
  getFullPath,
  getActionType,
  getAsyncActionTypes,
  mapSrcFile,
  mapComponent,
  mapReduxFile,
  mapReduxTestFile,
  mapFeatureFile,
  mapTestFile,
  mapComponentTestFile,
  joinPath,
  getFeatures,
  fatalError,
  setSilent,
  log,
  warn,
  error,

  getFeatureName,
};