UNPKG

14.2 kBJavaScriptView Raw
1#!/usr/bin/env node
2"use strict";
3/**
4 * @license
5 * Copyright Google LLC 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 */
10var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11 if (k2 === undefined) k2 = k;
12 Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
13}) : (function(o, m, k, k2) {
14 if (k2 === undefined) k2 = k;
15 o[k2] = m[k];
16}));
17var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
18 Object.defineProperty(o, "default", { enumerable: true, value: v });
19}) : function(o, v) {
20 o["default"] = v;
21});
22var __importStar = (this && this.__importStar) || function (mod) {
23 if (mod && mod.__esModule) return mod;
24 var result = {};
25 if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
26 __setModuleDefault(result, mod);
27 return result;
28};
29var __importDefault = (this && this.__importDefault) || function (mod) {
30 return (mod && mod.__esModule) ? mod : { "default": mod };
31};
32Object.defineProperty(exports, "__esModule", { value: true });
33exports.main = void 0;
34// symbol polyfill must go first
35require("symbol-observable");
36const core_1 = require("@angular-devkit/core");
37const node_1 = require("@angular-devkit/core/node");
38const schematics_1 = require("@angular-devkit/schematics");
39const tools_1 = require("@angular-devkit/schematics/tools");
40const ansiColors = __importStar(require("ansi-colors"));
41const inquirer = __importStar(require("inquirer"));
42const minimist_1 = __importDefault(require("minimist"));
43/**
44 * Parse the name of schematic passed in argument, and return a {collection, schematic} named
45 * tuple. The user can pass in `collection-name:schematic-name`, and this function will either
46 * return `{collection: 'collection-name', schematic: 'schematic-name'}`, or it will error out
47 * and show usage.
48 *
49 * In the case where a collection name isn't part of the argument, the default is to use the
50 * schematics package (@angular-devkit/schematics-cli) as the collection.
51 *
52 * This logic is entirely up to the tooling.
53 *
54 * @param str The argument to parse.
55 * @return {{collection: string, schematic: (string)}}
56 */
57function parseSchematicName(str) {
58 let collection = '@angular-devkit/schematics-cli';
59 let schematic = str;
60 if (schematic && schematic.indexOf(':') != -1) {
61 [collection, schematic] = [
62 schematic.slice(0, schematic.lastIndexOf(':')),
63 schematic.substring(schematic.lastIndexOf(':') + 1),
64 ];
65 }
66 return { collection, schematic };
67}
68function _listSchematics(workflow, collectionName, logger) {
69 try {
70 const collection = workflow.engine.createCollection(collectionName);
71 logger.info(collection.listSchematicNames().join('\n'));
72 }
73 catch (error) {
74 logger.fatal(error.message);
75 return 1;
76 }
77 return 0;
78}
79function _createPromptProvider() {
80 return (definitions) => {
81 const questions = definitions.map((definition) => {
82 const question = {
83 name: definition.id,
84 message: definition.message,
85 default: definition.default,
86 };
87 const validator = definition.validator;
88 if (validator) {
89 question.validate = (input) => validator(input);
90 }
91 switch (definition.type) {
92 case 'confirmation':
93 return { ...question, type: 'confirm' };
94 case 'list':
95 return {
96 ...question,
97 type: definition.multiselect ? 'checkbox' : 'list',
98 choices: definition.items &&
99 definition.items.map((item) => {
100 if (typeof item == 'string') {
101 return item;
102 }
103 else {
104 return {
105 name: item.label,
106 value: item.value,
107 };
108 }
109 }),
110 };
111 default:
112 return { ...question, type: definition.type };
113 }
114 });
115 return inquirer.prompt(questions);
116 };
117}
118// eslint-disable-next-line max-lines-per-function
119async function main({ args, stdout = process.stdout, stderr = process.stderr, }) {
120 const argv = parseArgs(args);
121 // Create a separate instance to prevent unintended global changes to the color configuration
122 // Create function is not defined in the typings. See: https://github.com/doowb/ansi-colors/pull/44
123 const colors = ansiColors.create();
124 /** Create the DevKit Logger used through the CLI. */
125 const logger = (0, node_1.createConsoleLogger)(argv['verbose'], stdout, stderr, {
126 info: (s) => s,
127 debug: (s) => s,
128 warn: (s) => colors.bold.yellow(s),
129 error: (s) => colors.bold.red(s),
130 fatal: (s) => colors.bold.red(s),
131 });
132 if (argv.help) {
133 logger.info(getUsage());
134 return 0;
135 }
136 /** Get the collection an schematic name from the first argument. */
137 const { collection: collectionName, schematic: schematicName } = parseSchematicName(argv._.shift() || null);
138 const isLocalCollection = collectionName.startsWith('.') || collectionName.startsWith('/');
139 /** Gather the arguments for later use. */
140 const debugPresent = argv['debug'] !== null;
141 const debug = debugPresent ? !!argv['debug'] : isLocalCollection;
142 const dryRunPresent = argv['dry-run'] !== null;
143 const dryRun = dryRunPresent ? !!argv['dry-run'] : debug;
144 const force = argv['force'];
145 const allowPrivate = argv['allow-private'];
146 /** Create the workflow scoped to the working directory that will be executed with this run. */
147 const workflow = new tools_1.NodeWorkflow(process.cwd(), {
148 force,
149 dryRun,
150 resolvePaths: [process.cwd(), __dirname],
151 schemaValidation: true,
152 });
153 /** If the user wants to list schematics, we simply show all the schematic names. */
154 if (argv['list-schematics']) {
155 return _listSchematics(workflow, collectionName, logger);
156 }
157 if (!schematicName) {
158 logger.info(getUsage());
159 return 1;
160 }
161 if (debug) {
162 logger.info(`Debug mode enabled${isLocalCollection ? ' by default for local collections' : ''}.`);
163 }
164 // Indicate to the user when nothing has been done. This is automatically set to off when there's
165 // a new DryRunEvent.
166 let nothingDone = true;
167 // Logging queue that receives all the messages to show the users. This only get shown when no
168 // errors happened.
169 let loggingQueue = [];
170 let error = false;
171 /**
172 * Logs out dry run events.
173 *
174 * All events will always be executed here, in order of discovery. That means that an error would
175 * be shown along other events when it happens. Since errors in workflows will stop the Observable
176 * from completing successfully, we record any events other than errors, then on completion we
177 * show them.
178 *
179 * This is a simple way to only show errors when an error occur.
180 */
181 workflow.reporter.subscribe((event) => {
182 nothingDone = false;
183 // Strip leading slash to prevent confusion.
184 const eventPath = event.path.startsWith('/') ? event.path.substr(1) : event.path;
185 switch (event.kind) {
186 case 'error':
187 error = true;
188 const desc = event.description == 'alreadyExist' ? 'already exists' : 'does not exist';
189 logger.error(`ERROR! ${eventPath} ${desc}.`);
190 break;
191 case 'update':
192 loggingQueue.push(`${colors.cyan('UPDATE')} ${eventPath} (${event.content.length} bytes)`);
193 break;
194 case 'create':
195 loggingQueue.push(`${colors.green('CREATE')} ${eventPath} (${event.content.length} bytes)`);
196 break;
197 case 'delete':
198 loggingQueue.push(`${colors.yellow('DELETE')} ${eventPath}`);
199 break;
200 case 'rename':
201 const eventToPath = event.to.startsWith('/') ? event.to.substr(1) : event.to;
202 loggingQueue.push(`${colors.blue('RENAME')} ${eventPath} => ${eventToPath}`);
203 break;
204 }
205 });
206 /**
207 * Listen to lifecycle events of the workflow to flush the logs between each phases.
208 */
209 workflow.lifeCycle.subscribe((event) => {
210 if (event.kind == 'workflow-end' || event.kind == 'post-tasks-start') {
211 if (!error) {
212 // Flush the log queue and clean the error state.
213 loggingQueue.forEach((log) => logger.info(log));
214 }
215 loggingQueue = [];
216 error = false;
217 }
218 });
219 /**
220 * Remove every options from argv that we support in schematics itself.
221 */
222 const parsedArgs = Object.assign({}, argv);
223 delete parsedArgs['--'];
224 for (const key of booleanArgs) {
225 delete parsedArgs[key];
226 }
227 /**
228 * Add options from `--` to args.
229 */
230 const argv2 = (0, minimist_1.default)(argv['--']);
231 for (const key of Object.keys(argv2)) {
232 parsedArgs[key] = argv2[key];
233 }
234 // Show usage of deprecated options
235 workflow.registry.useXDeprecatedProvider((msg) => logger.warn(msg));
236 // Pass the rest of the arguments as the smart default "argv". Then delete it.
237 workflow.registry.addSmartDefaultProvider('argv', (schema) => {
238 if ('index' in schema) {
239 return argv._[Number(schema['index'])];
240 }
241 else {
242 return argv._;
243 }
244 });
245 delete parsedArgs._;
246 // Add prompts.
247 if (argv['interactive'] && isTTY()) {
248 workflow.registry.usePromptProvider(_createPromptProvider());
249 }
250 /**
251 * Execute the workflow, which will report the dry run events, run the tasks, and complete
252 * after all is done.
253 *
254 * The Observable returned will properly cancel the workflow if unsubscribed, error out if ANY
255 * step of the workflow failed (sink or task), with details included, and will only complete
256 * when everything is done.
257 */
258 try {
259 await workflow
260 .execute({
261 collection: collectionName,
262 schematic: schematicName,
263 options: parsedArgs,
264 allowPrivate: allowPrivate,
265 debug: debug,
266 logger: logger,
267 })
268 .toPromise();
269 if (nothingDone) {
270 logger.info('Nothing to be done.');
271 }
272 else if (dryRun) {
273 logger.info(`Dry run enabled${dryRunPresent ? '' : ' by default in debug mode'}. No files written to disk.`);
274 }
275 return 0;
276 }
277 catch (err) {
278 if (err instanceof schematics_1.UnsuccessfulWorkflowExecution) {
279 // "See above" because we already printed the error.
280 logger.fatal('The Schematic workflow failed. See above.');
281 }
282 else if (debug) {
283 logger.fatal('An error occured:\n' + err.stack);
284 }
285 else {
286 logger.fatal(err.stack || err.message);
287 }
288 return 1;
289 }
290}
291exports.main = main;
292/**
293 * Get usage of the CLI tool.
294 */
295function getUsage() {
296 return core_1.tags.stripIndent `
297 schematics [CollectionName:]SchematicName [options, ...]
298
299 By default, if the collection name is not specified, use the internal collection provided
300 by the Schematics CLI.
301
302 Options:
303 --debug Debug mode. This is true by default if the collection is a relative
304 path (in that case, turn off with --debug=false).
305
306 --allow-private Allow private schematics to be run from the command line. Default to
307 false.
308
309 --dry-run Do not output anything, but instead just show what actions would be
310 performed. Default to true if debug is also true.
311
312 --force Force overwriting files that would otherwise be an error.
313
314 --list-schematics List all schematics from the collection, by name. A collection name
315 should be suffixed by a colon. Example: '@angular-devkit/schematics-cli:'.
316
317 --no-interactive Disables interactive input prompts.
318
319 --verbose Show more information.
320
321 --help Show this message.
322
323 Any additional option is passed to the Schematics depending on its schema.
324 `;
325}
326/** Parse the command line. */
327const booleanArgs = [
328 'allowPrivate',
329 'allow-private',
330 'debug',
331 'dry-run',
332 'dryRun',
333 'force',
334 'help',
335 'list-schematics',
336 'listSchematics',
337 'verbose',
338 'interactive',
339];
340function parseArgs(args) {
341 return (0, minimist_1.default)(args, {
342 boolean: booleanArgs,
343 alias: {
344 'dryRun': 'dry-run',
345 'listSchematics': 'list-schematics',
346 'allowPrivate': 'allow-private',
347 },
348 default: {
349 'interactive': true,
350 'debug': null,
351 'dryRun': null,
352 },
353 '--': true,
354 });
355}
356function isTTY() {
357 const isTruthy = (value) => {
358 // Returns true if value is a string that is anything but 0 or false.
359 return value !== undefined && value !== '0' && value.toUpperCase() !== 'FALSE';
360 };
361 // If we force TTY, we always return true.
362 const force = process.env['NG_FORCE_TTY'];
363 if (force !== undefined) {
364 return isTruthy(force);
365 }
366 return !!process.stdout.isTTY && !isTruthy(process.env['CI']);
367}
368if (require.main === module) {
369 const args = process.argv.slice(2);
370 main({ args })
371 .then((exitCode) => (process.exitCode = exitCode))
372 .catch((e) => {
373 throw e;
374 });
375}