packages/mastery/new/components/NewCommand.js

import {
  Spinner,
} from 'cli-spinner';
import _ from 'lodash';
import del from 'del';
import {
  execSync,
} from 'child_process';
import fs from 'fs';
import inquirer from 'inquirer';
import path from 'path';
import simpleGit from 'simple-git';

const ColorizeText = requireF('services/ColorizeText');
const {
  getTargetPackage,
} = requireF('services/CommonServices');

/**
 * The main class that handles the 'new' command execution.
 *
 * @export
 * @class NewCommand
 * @property {string} MK_INTRO The translation key of 'initializing' phase message
 * @property {string} MK_CLONING The translation key of 'cloning' phase message
 * @property {string} MK_INSTALLING The translation key of 'installing' phase message
 * @property {string} CK_REPO The configuration hierarchical key to retrieve from nconf
 * @property {string[]} DELETE_FILES Files to delete after cloning
 * @property {Object[]} QUESTIONS Inquirer questions objects
 */
export default class NewCommand {
  MK_INTRO = 'mastery.new.intro';
  MK_CLONING = 'mastery.new.cloning'
  MK_INSTALLING = 'mastery.new.installing';
  CK_REPO = 'repoUrl';
  DELETE_FILES = [
    '.git',
    '.travis.yml',
    'README.md',
  ];

  /**
   * Give user some questions to fill some package.json keys.
   *
   * @returns {Promise.<Object[]>} User answers, with question key as its key and answer as its value
   */
  async querying() {
    this.questions = [{
      name: 'name',
      message: i18n.t('mastery.questions.new.name'),
      default: _.kebabCase(path.basename(this.resolvedDestination)),
    }, {
      name: 'version',
      message: i18n.t('mastery.questions.new.version'),
      default: '0.1.0',
    }, {
      name: 'license',
      message: i18n.t('mastery.questions.new.license'),
    }, {
      name: 'author',
      message: i18n.t('mastery.questions.new.author'),
    }, {
      name: 'email',
      message: i18n.t('mastery.questions.new.email'),
    }, {
      name: 'url',
      message: i18n.t('mastery.questions.new.url'),
    }];
    return await inquirer.prompt(this.questions);
  }

  /**
   * Clone latest project version from MasteryJS github repo.
   *
   * @returns {Promise}
   */
  async clone() {
    const git = simpleGit();
    const repoUrl = conf.get(this.CK_REPO);

    await git.clone(repoUrl, path.resolve(this.resolvedDestination));
  }

  /**
   * Install npm packages for both dev and production dependencies.
   */
  install() {
    execSync('npm install', {
      stdio: 'inherit',
    });
  }
  /**
   * Write a new package.json file merged with user answers before.
   */
  writeJSONPackage() {
    const questionKeys = _.map(this.questions, 'name');
    const answerValues = _.pick(this.answers, questionKeys);
    const targetPackage = getTargetPackage(this.resolvedDestination);

    _.forEach(answerValues, (val, key) => {
      _.unset(targetPackage, key); // delete keys that exists in question
    });

    // merge answer values with default package.json from repo
    const newPackage = _.merge(_.clone(answerValues), targetPackage);

    fs.writeFileSync(
      path.join(this.resolvedDestination, 'package.json'),
      JSON.stringify(newPackage, null, 2),
    );
  }

  /**
   * Delete unnecessary files after cloning.
   */
  cleanFiles = () => {
    del.sync(this.DELETE_FILES);
  }

  /**
   * Show some guidance text to help user deciding what to do next.
   */
  guidance() {
    console.log(ColorizeText.info(
      ' *************************************************************\n',
      '*************************************************************\n',
      '**                                                         **\n',
      '**                  WELCOME TO MASTERYJS                   **\n',
      '**                                                         **\n',
      '*************************************************************\n',
      '*************************************************************\n',
      '**\n',
      '**  To kick start your installation, type the following command first:\n',
      `**  >_ cd ${this.destDir}\n`,
      '**\n',
      '**  A few tips to running your project in development mode:\n',
      '**  >_ mastery serve (running from source)\n',
      '**  >_ mastery serve -i (running & debugging from source using chrome inspector)\n',
      '**  >_ mastery serve -p <debug-port> (running & debugging from source using any IDE debugger)\n',
      '**\n',
      '**  To deploy your project, first you need to build using the following command:\n',
      '**  >_ mastery build\n',
      '**\n',
      '**  This commands will be available inside build directory:\n',
      '**  >_ mastery start\n',
      '**  >_ mastery stop\n',
      '**  >_ mastery restart\n',
      '**\n',
      '**  Remember to always type the following command if you get lost!\n',
      '**  >_ mastery --help\n',
      '**\n',
      '*************************************************************\n',
      '*************************************************************\n',
    ));
  }

  /**
   * The main method to call another methods sequentially, including decorations output.
   *
   * @param {string} destDir A relative destination directory path
   * @returns {Promise}
   */
  async execute(destDir: string) {
    this.destDir = destDir;
    this.resolvedDestination = path.resolve(path.join(destDir));

    console.log(ColorizeText.info(i18n.t(this.MK_INTRO)));
    console.log('');

    this.answers = await this.querying();

    console.log('');

    const loading = new Spinner(ColorizeText.info(i18n.t(this.MK_CLONING)));
    loading.setSpinnerString(constants.DEFAULT_SPINNER);
    loading.start();

    await this.clone();

    loading.stop(true);

    console.log(ColorizeText.info(i18n.t(this.MK_INSTALLING)));
    console.log('');

    // for next actions, we have to be inside the build directory
    process.chdir(this.resolvedDestination);

    this.cleanFiles();

    this.writeJSONPackage();

    this.install();

    console.log('');
    this.guidance();
  }
}