command-runner.js 8.23 KB
Newer Older
Kriengkrai Yothee's avatar
Kriengkrai Yothee committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
/**
 * @license
 * Copyright Google Inc. All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.io/license
 */
const core_1 = require("@angular-devkit/core");
const debug = require("debug");
const fs_1 = require("fs");
const path_1 = require("path");
const json_schema_1 = require("../utilities/json-schema");
const analytics_1 = require("./analytics");
const command_1 = require("./command");
const parser = require("./parser");
const analyticsDebug = debug('ng:analytics:commands');
// NOTE: Update commands.json if changing this.  It's still deep imported in one CI validation
const standardCommands = {
    'add': '../commands/add.json',
    'analytics': '../commands/analytics.json',
    'build': '../commands/build.json',
    'deploy': '../commands/deploy.json',
    'config': '../commands/config.json',
    'doc': '../commands/doc.json',
    'e2e': '../commands/e2e.json',
    'make-this-awesome': '../commands/easter-egg.json',
    'generate': '../commands/generate.json',
    'get': '../commands/deprecated.json',
    'set': '../commands/deprecated.json',
    'help': '../commands/help.json',
    'lint': '../commands/lint.json',
    'new': '../commands/new.json',
    'run': '../commands/run.json',
    'serve': '../commands/serve.json',
    'test': '../commands/test.json',
    'update': '../commands/update.json',
    'version': '../commands/version.json',
    'xi18n': '../commands/xi18n.json',
};
/**
 * Create the analytics instance.
 * @private
 */
async function _createAnalytics(workspace, skipPrompt = false) {
    let config = await analytics_1.getGlobalAnalytics();
    // If in workspace and global analytics is enabled, defer to workspace level
    if (workspace && config) {
        const skipAnalytics = skipPrompt ||
            (process.env['NG_CLI_ANALYTICS'] &&
                (process.env['NG_CLI_ANALYTICS'].toLowerCase() === 'false' ||
                    process.env['NG_CLI_ANALYTICS'] === '0'));
        // TODO: This should honor the `no-interactive` option.
        //       It is currently not an `ng` option but rather only an option for specific commands.
        //       The concept of `ng`-wide options are needed to cleanly handle this.
        if (!skipAnalytics && !(await analytics_1.hasWorkspaceAnalyticsConfiguration())) {
            await analytics_1.promptProjectAnalytics();
        }
        config = await analytics_1.getWorkspaceAnalytics();
    }
    const maybeSharedAnalytics = await analytics_1.getSharedAnalytics();
    if (config && maybeSharedAnalytics) {
        return new core_1.analytics.MultiAnalytics([config, maybeSharedAnalytics]);
    }
    else if (config) {
        return config;
    }
    else if (maybeSharedAnalytics) {
        return maybeSharedAnalytics;
    }
    else {
        return new core_1.analytics.NoopAnalytics();
    }
}
async function loadCommandDescription(name, path, registry) {
    const schemaPath = path_1.resolve(__dirname, path);
    const schemaContent = fs_1.readFileSync(schemaPath, 'utf-8');
    const schema = core_1.json.parseJson(schemaContent, core_1.JsonParseMode.Loose, { path: schemaPath });
    if (!core_1.isJsonObject(schema)) {
        throw new Error('Invalid command JSON loaded from ' + JSON.stringify(schemaPath));
    }
    return json_schema_1.parseJsonSchemaToCommandDescription(name, schemaPath, registry, schema);
}
/**
 * Run a command.
 * @param args Raw unparsed arguments.
 * @param logger The logger to use.
 * @param workspace Workspace information.
 * @param commands The map of supported commands.
 * @param options Additional options.
 */
async function runCommand(args, logger, workspace, commands = standardCommands, options = {}) {
    // This registry is exclusively used for flattening schemas, and not for validating.
    const registry = new core_1.schema.CoreSchemaRegistry([]);
    registry.registerUriHandler((uri) => {
        if (uri.startsWith('ng-cli://')) {
            const content = fs_1.readFileSync(path_1.join(__dirname, '..', uri.substr('ng-cli://'.length)), 'utf-8');
            return Promise.resolve(JSON.parse(content));
        }
        else {
            return null;
        }
    });
    let commandName = undefined;
    for (let i = 0; i < args.length; i++) {
        const arg = args[i];
        if (!arg.startsWith('-')) {
            commandName = arg;
            args.splice(i, 1);
            break;
        }
    }
    let description = null;
    // if no commands were found, use `help`.
    if (!commandName) {
        if (args.length === 1 && args[0] === '--version') {
            commandName = 'version';
        }
        else {
            commandName = 'help';
        }
        if (!(commandName in commands)) {
            logger.error(core_1.tags.stripIndent `
          The "${commandName}" command seems to be disabled.
          This is an issue with the CLI itself. If you see this comment, please report it and
          provide your repository.
        `);
            return 1;
        }
    }
    if (commandName in commands) {
        description = await loadCommandDescription(commandName, commands[commandName], registry);
    }
    else {
        const commandNames = Object.keys(commands);
        // Optimize loading for common aliases
        if (commandName.length === 1) {
            commandNames.sort((a, b) => {
                const aMatch = a[0] === commandName;
                const bMatch = b[0] === commandName;
                if (aMatch && !bMatch) {
                    return -1;
                }
                else if (!aMatch && bMatch) {
                    return 1;
                }
                else {
                    return 0;
                }
            });
        }
        for (const name of commandNames) {
            const aliasDesc = await loadCommandDescription(name, commands[name], registry);
            const aliases = aliasDesc.aliases;
            if (aliases && aliases.some(alias => alias === commandName)) {
                commandName = name;
                description = aliasDesc;
                break;
            }
        }
    }
    if (!description) {
        const commandsDistance = {};
        const name = commandName;
        const allCommands = Object.keys(commands).sort((a, b) => {
            if (!(a in commandsDistance)) {
                commandsDistance[a] = core_1.strings.levenshtein(a, name);
            }
            if (!(b in commandsDistance)) {
                commandsDistance[b] = core_1.strings.levenshtein(b, name);
            }
            return commandsDistance[a] - commandsDistance[b];
        });
        logger.error(core_1.tags.stripIndent `
        The specified command ("${commandName}") is invalid. For a list of available options,
        run "ng help".

        Did you mean "${allCommands[0]}"?
    `);
        return 1;
    }
    try {
        const parsedOptions = parser.parseArguments(args, description.options, logger);
        command_1.Command.setCommandMap(async () => {
            const map = {};
            for (const [name, path] of Object.entries(commands)) {
                map[name] = await loadCommandDescription(name, path, registry);
            }
            return map;
        });
        const analytics = options.analytics ||
            (await _createAnalytics(!!workspace.configFile, description.name === 'update'));
        const context = { workspace, analytics };
        const command = new description.impl(context, description, logger);
        // Flush on an interval (if the event loop is waiting).
        let analyticsFlushPromise = Promise.resolve();
        setInterval(() => {
            analyticsFlushPromise = analyticsFlushPromise.then(() => analytics.flush());
        }, 1000);
        const result = await command.validateAndRun(parsedOptions);
        // Flush one last time.
        await analyticsFlushPromise.then(() => analytics.flush());
        return result;
    }
    catch (e) {
        if (e instanceof parser.ParseArgumentException) {
            logger.fatal('Cannot parse arguments. See below for the reasons.');
            logger.fatal('    ' + e.comments.join('\n    '));
            return 1;
        }
        else {
            throw e;
        }
    }
}
exports.runCommand = runCommand;