ember-cli/ember-cli

View on GitHub
lib/cli/cli.js

Summary

Maintainability
B
5 hrs
Test Coverage
A
95%
'use strict';

const lookupCommand = require('./lookup-command');
const getOptionArgs = require('../utilities/get-option-args');
const hash = require('promise.hash.helper');
const logger = require('heimdalljs-logger')('ember-cli:cli');
const loggerTesting = require('heimdalljs-logger')('ember-cli:testing');
const Instrumentation = require('../models/instrumentation');
const PackageInfoCache = require('../models/package-info-cache');
const heimdall = require('heimdalljs');

const onProcessInterrupt = require('../utilities/will-interrupt-process');

class CLI {
  /**
   * @private
   * @class CLI
   * @constructor
   * @param options
   */
  constructor(options) {
    /**
     * @private
     * @property name
     */
    this.name = options.name;

    /**
     * @private
     * @property ui
     * @type UI
     */
    this.ui = options.ui;

    /**
     * @private
     * @property testing
     * @type Boolean
     */
    this.testing = options.testing;

    /**
     * @private
     * @property disableDependencyChecker
     * @type Boolean
     */
    this.disableDependencyChecker = options.disableDependencyChecker;

    /**
     * @private
     * @property root
     */
    this.root = options.root;

    /**
     * @private
     * @property npmPackage
     */
    this.npmPackage = options.npmPackage;

    /**
     * @private
     * @property instrumentation
     */
    this.instrumentation =
      options.instrumentation ||
      new Instrumentation({
        ui: options.ui,
        initInstrumentation: options.initInstrumentation,
      });

    this.packageInfoCache = new PackageInfoCache(this.ui);

    logger.info('testing %o', !!this.testing);
  }

  /**
   * @private
   * @method maybeMakeCommand
   * @param commandName
   * @param commandArgs
   * @return {null|CurrentCommand}
   */
  maybeMakeCommand(commandName, commandArgs) {
    if (this._environment === undefined) {
      throw new Error('Unable to make command without environment, you have to execute "run" method first.');
    }
    let CurrentCommand = lookupCommand(this._environment.commands, commandName, commandArgs, {
      project: this._environment.project,
      ui: this.ui,
    });

    /*
     * XXX Need to decide what to do here about showing errors. For
     * a non-CLI project the cache is local and probably should. For
     * a CLI project the cache is there, but not sure when we'll know
     * about all the errors, because there may be multiple projects.
     *   if (this.packageInfoCache.hasErrors()) {
     *     this.packageInfoCache.showErrors();
     *   }
     */
    let command = new CurrentCommand({
      ui: this.ui,
      commands: this._environment.commands,
      tasks: this._environment.tasks,
      project: this._environment.project,
      settings: this._environment.settings,
      testing: this.testing,
      cli: this,
    });

    return command;
  }

  /**
   * @private
   * @method run
   * @param {Promise<object>} environmentPromiseHash
   * @return {Promise}
   */
  async run(environmentPromiseHash) {
    if (environmentPromiseHash === undefined) {
      return Promise.reject(new Error('Unable to execute "run" command without environment argument'));
    }
    let shutdownOnExit = null;

    let environment = (this._environment = await hash(environmentPromiseHash));

    try {
      let args = environment.cliArgs.slice();
      let commandName = args.shift();
      let commandArgs = args;
      let helpOptions;

      let commandLookupCreationToken = heimdall.start('lookup-command');

      let command = this.maybeMakeCommand(commandName, commandArgs);

      commandLookupCreationToken.stop();

      getOptionArgs('--verbose', commandArgs).forEach((arg) => {
        process.env[`EMBER_VERBOSE_${arg.toUpperCase()}`] = 'true';
      });

      logger.info('command: %s', commandName);

      let instrumentation = this.instrumentation;
      let onCommandInterrupt;

      let runPromise = Promise.resolve().then(async () => {
        let resultOrExitCode;

        try {
          instrumentation.stopAndReport('init');

          try {
            instrumentation.start('command');

            loggerTesting.info('cli: command.beforeRun');
            onProcessInterrupt.addHandler(onCommandInterrupt);

            await command.beforeRun(commandArgs);

            loggerTesting.info('cli: command.validateAndRun');

            resultOrExitCode = await command.validateAndRun(commandArgs);
          } finally {
            instrumentation.stopAndReport('command', commandName, commandArgs);

            onProcessInterrupt.removeHandler(onCommandInterrupt);
          }
        } finally {
          instrumentation.start('shutdown');
          shutdownOnExit = function () {
            instrumentation.stopAndReport('shutdown');
          };
        }

        // if the help option was passed, call the help command
        if (resultOrExitCode === 'callHelp') {
          helpOptions = {
            environment,
            commandName,
            commandArgs,
          };

          resultOrExitCode = await this.callHelp(helpOptions);
        }

        let exitCode = resultOrExitCode;

        loggerTesting.info(`cli: command run complete. exitCode: ${exitCode}`);
        // TODO: fix this
        // Possibly this issue: https://github.com/joyent/node/issues/8329
        // Wait to resolve promise when running on windows.
        // This ensures that stdout is flushed so acceptance tests get full output

        if (process.platform === 'win32') {
          return new Promise((resolve) => {
            setTimeout(resolve, 250, exitCode);
          });
        } else {
          return exitCode;
        }
      });

      onCommandInterrupt = async () => {
        await command.onInterrupt();

        return await runPromise;
      };

      return await runPromise;
    } catch (error) {
      this.logError(error);
      return 1;
    } finally {
      if (shutdownOnExit) {
        shutdownOnExit();
      }
    }
  }

  /**
   * @private
   * @method callHelp
   * @param options
   * @return {Promise}
   */
  callHelp(options) {
    let environment = options.environment;
    let commandName = options.commandName;
    let commandArgs = options.commandArgs;
    let helpIndex = commandArgs.indexOf('--help');
    let hIndex = commandArgs.indexOf('-h');

    let HelpCommand = lookupCommand(environment.commands, 'help', commandArgs, {
      project: environment.project,
      ui: this.ui,
    });

    let help = new HelpCommand({
      ui: this.ui,
      commands: environment.commands,
      tasks: environment.tasks,
      project: environment.project,
      settings: environment.settings,
      testing: this.testing,
    });

    if (helpIndex > -1) {
      commandArgs.splice(helpIndex, 1);
    }

    if (hIndex > -1) {
      commandArgs.splice(hIndex, 1);
    }

    commandArgs.unshift(commandName);

    return help.validateAndRun(commandArgs);
  }

  /**
   * @private
   * @method logError
   * @param error
   * @return {number}
   */
  logError(error) {
    if (this.testing && error) {
      console.error(error.message);
      if (error.stack) {
        console.error(error.stack);
      }
      throw error;
    }
    this.ui.errorLog.push(error);
    this.ui.writeError(error);
    return 1;
  }
}

module.exports = CLI;