UNPKG

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