UNPKG

21.4 kBJavaScriptView Raw
1"use strict";
2/**
3 * @license
4 * Copyright Google LLC All Rights Reserved.
5 *
6 * Use of this source code is governed by an MIT-style license that can be
7 * found in the LICENSE file at https://angular.io/license
8 */
9Object.defineProperty(exports, "__esModule", { value: true });
10exports.SchematicCommand = exports.UnknownCollectionError = void 0;
11const core_1 = require("@angular-devkit/core");
12const schematics_1 = require("@angular-devkit/schematics");
13const tools_1 = require("@angular-devkit/schematics/tools");
14const inquirer = require("inquirer");
15const systemPath = require("path");
16const color_1 = require("../utilities/color");
17const config_1 = require("../utilities/config");
18const json_schema_1 = require("../utilities/json-schema");
19const package_manager_1 = require("../utilities/package-manager");
20const tty_1 = require("../utilities/tty");
21const analytics_1 = require("./analytics");
22const command_1 = require("./command");
23const parser_1 = require("./parser");
24const schematic_engine_host_1 = require("./schematic-engine-host");
25class UnknownCollectionError extends Error {
26 constructor(collectionName) {
27 super(`Invalid collection (${collectionName}).`);
28 }
29}
30exports.UnknownCollectionError = UnknownCollectionError;
31class SchematicCommand extends command_1.Command {
32 constructor(context, description, logger) {
33 super(context, description, logger);
34 this.allowPrivateSchematics = false;
35 this.useReportAnalytics = false;
36 this.defaultCollectionName = '@schematics/angular';
37 this.collectionName = this.defaultCollectionName;
38 }
39 async initialize(options) {
40 await this.createWorkflow(options);
41 if (this.schematicName) {
42 // Set the options.
43 const collection = this.getCollection(this.collectionName);
44 const schematic = this.getSchematic(collection, this.schematicName, true);
45 const options = await json_schema_1.parseJsonSchemaToOptions(this._workflow.registry, schematic.description.schemaJson || {});
46 this.description.description = schematic.description.description;
47 this.description.options.push(...options.filter((x) => !x.hidden));
48 // Remove any user analytics from schematics that are NOT part of our safelist.
49 for (const o of this.description.options) {
50 if (o.userAnalytics && !analytics_1.isPackageNameSafeForAnalytics(this.collectionName)) {
51 o.userAnalytics = undefined;
52 }
53 }
54 }
55 }
56 async printHelp() {
57 await super.printHelp();
58 this.logger.info('');
59 const subCommandOption = this.description.options.filter((x) => x.subcommands)[0];
60 if (!subCommandOption || !subCommandOption.subcommands) {
61 return 0;
62 }
63 const schematicNames = Object.keys(subCommandOption.subcommands);
64 if (schematicNames.length > 1) {
65 this.logger.info('Available Schematics:');
66 const namesPerCollection = {};
67 schematicNames.forEach((name) => {
68 let [collectionName, schematicName] = name.split(/:/, 2);
69 if (!schematicName) {
70 schematicName = collectionName;
71 collectionName = this.collectionName;
72 }
73 if (!namesPerCollection[collectionName]) {
74 namesPerCollection[collectionName] = [];
75 }
76 namesPerCollection[collectionName].push(schematicName);
77 });
78 const defaultCollection = await this.getDefaultSchematicCollection();
79 Object.keys(namesPerCollection).forEach((collectionName) => {
80 const isDefault = defaultCollection == collectionName;
81 this.logger.info(` Collection "${collectionName}"${isDefault ? ' (default)' : ''}:`);
82 namesPerCollection[collectionName].forEach((schematicName) => {
83 this.logger.info(` ${schematicName}`);
84 });
85 });
86 }
87 return 0;
88 }
89 async printHelpUsage() {
90 const subCommandOption = this.description.options.filter((x) => x.subcommands)[0];
91 if (!subCommandOption || !subCommandOption.subcommands) {
92 return;
93 }
94 const schematicNames = Object.keys(subCommandOption.subcommands);
95 if (schematicNames.length == 1) {
96 this.logger.info(this.description.description);
97 const opts = this.description.options.filter((x) => x.positional === undefined);
98 const [collectionName, schematicName] = schematicNames[0].split(/:/)[0];
99 // Display <collectionName:schematicName> if this is not the default collectionName,
100 // otherwise just show the schematicName.
101 const displayName = collectionName == (await this.getDefaultSchematicCollection())
102 ? schematicName
103 : schematicNames[0];
104 const schematicOptions = subCommandOption.subcommands[schematicNames[0]].options;
105 const schematicArgs = schematicOptions.filter((x) => x.positional !== undefined);
106 const argDisplay = schematicArgs.length > 0
107 ? ' ' + schematicArgs.map((a) => `<${core_1.strings.dasherize(a.name)}>`).join(' ')
108 : '';
109 this.logger.info(core_1.tags.oneLine `
110 usage: ng ${this.description.name} ${displayName}${argDisplay}
111 ${opts.length > 0 ? `[options]` : ``}
112 `);
113 this.logger.info('');
114 }
115 else {
116 await super.printHelpUsage();
117 }
118 }
119 getEngine() {
120 return this._workflow.engine;
121 }
122 getCollection(collectionName) {
123 const engine = this.getEngine();
124 const collection = engine.createCollection(collectionName);
125 if (collection === null) {
126 throw new UnknownCollectionError(collectionName);
127 }
128 return collection;
129 }
130 getSchematic(collection, schematicName, allowPrivate) {
131 return collection.createSchematic(schematicName, allowPrivate);
132 }
133 setPathOptions(options, workingDir) {
134 if (workingDir === '') {
135 return {};
136 }
137 return options
138 .filter((o) => o.format === 'path')
139 .map((o) => o.name)
140 .reduce((acc, curr) => {
141 acc[curr] = workingDir;
142 return acc;
143 }, {});
144 }
145 /*
146 * Runtime hook to allow specifying customized workflow
147 */
148 async createWorkflow(options) {
149 if (this._workflow) {
150 return this._workflow;
151 }
152 const { force, dryRun } = options;
153 const root = this.context.root;
154 const workflow = new tools_1.NodeWorkflow(root, {
155 force,
156 dryRun,
157 packageManager: await package_manager_1.getPackageManager(root),
158 packageRegistry: options.packageRegistry,
159 // A schema registry is required to allow customizing addUndefinedDefaults
160 registry: new core_1.schema.CoreSchemaRegistry(schematics_1.formats.standardFormats),
161 resolvePaths: this.workspace
162 ? // Workspace
163 this.collectionName === this.defaultCollectionName
164 ? // Favor __dirname for @schematics/angular to use the build-in version
165 [__dirname, process.cwd(), root]
166 : [process.cwd(), root, __dirname]
167 : // Global
168 [__dirname, process.cwd()],
169 schemaValidation: true,
170 optionTransforms: [
171 // Add configuration file defaults
172 async (schematic, current) => {
173 const projectName = typeof current.project === 'string'
174 ? current.project
175 : getProjectName();
176 return {
177 ...(await config_1.getSchematicDefaults(schematic.collection.name, schematic.name, projectName)),
178 ...current,
179 };
180 },
181 ],
182 engineHostCreator: (options) => new schematic_engine_host_1.SchematicEngineHost(options.resolvePaths),
183 });
184 const getProjectName = () => {
185 if (this.workspace) {
186 const projectNames = getProjectsByPath(this.workspace, process.cwd(), this.workspace.basePath);
187 if (projectNames.length === 1) {
188 return projectNames[0];
189 }
190 else {
191 if (projectNames.length > 1) {
192 this.logger.warn(core_1.tags.oneLine `
193 Two or more projects are using identical roots.
194 Unable to determine project using current working directory.
195 Using default workspace project instead.
196 `);
197 }
198 const defaultProjectName = this.workspace.extensions['defaultProject'];
199 if (typeof defaultProjectName === 'string' && defaultProjectName) {
200 return defaultProjectName;
201 }
202 }
203 }
204 return undefined;
205 };
206 workflow.registry.addPostTransform(core_1.schema.transforms.addUndefinedDefaults);
207 workflow.registry.addSmartDefaultProvider('projectName', getProjectName);
208 workflow.registry.useXDeprecatedProvider((msg) => this.logger.warn(msg));
209 let shouldReportAnalytics = true;
210 workflow.engineHost.registerOptionsTransform(async (_, options) => {
211 if (shouldReportAnalytics) {
212 shouldReportAnalytics = false;
213 await this.reportAnalytics([this.description.name], options);
214 }
215 return options;
216 });
217 if (options.interactive !== false && tty_1.isTTY()) {
218 workflow.registry.usePromptProvider((definitions) => {
219 const questions = definitions
220 .filter((definition) => !options.defaults || definition.default === undefined)
221 .map((definition) => {
222 var _a;
223 const question = {
224 name: definition.id,
225 message: definition.message,
226 default: definition.default,
227 };
228 const validator = definition.validator;
229 if (validator) {
230 question.validate = (input) => validator(input);
231 // Filter allows transformation of the value prior to validation
232 question.filter = async (input) => {
233 for (const type of definition.propertyTypes) {
234 let value;
235 switch (type) {
236 case 'string':
237 value = String(input);
238 break;
239 case 'integer':
240 case 'number':
241 value = Number(input);
242 break;
243 default:
244 value = input;
245 break;
246 }
247 // Can be a string if validation fails
248 const isValid = (await validator(value)) === true;
249 if (isValid) {
250 return value;
251 }
252 }
253 return input;
254 };
255 }
256 switch (definition.type) {
257 case 'confirmation':
258 question.type = 'confirm';
259 break;
260 case 'list':
261 question.type = definition.multiselect ? 'checkbox' : 'list';
262 question.choices = (_a = definition.items) === null || _a === void 0 ? void 0 : _a.map((item) => {
263 return typeof item == 'string'
264 ? item
265 : {
266 name: item.label,
267 value: item.value,
268 };
269 });
270 break;
271 default:
272 question.type = definition.type;
273 break;
274 }
275 return question;
276 });
277 return inquirer.prompt(questions);
278 });
279 }
280 return (this._workflow = workflow);
281 }
282 async getDefaultSchematicCollection() {
283 let workspace = await config_1.getWorkspace('local');
284 if (workspace) {
285 const project = config_1.getProjectByCwd(workspace);
286 if (project && workspace.getProjectCli(project)) {
287 const value = workspace.getProjectCli(project)['defaultCollection'];
288 if (typeof value == 'string') {
289 return value;
290 }
291 }
292 if (workspace.getCli()) {
293 const value = workspace.getCli()['defaultCollection'];
294 if (typeof value == 'string') {
295 return value;
296 }
297 }
298 }
299 workspace = await config_1.getWorkspace('global');
300 if (workspace && workspace.getCli()) {
301 const value = workspace.getCli()['defaultCollection'];
302 if (typeof value == 'string') {
303 return value;
304 }
305 }
306 return this.defaultCollectionName;
307 }
308 async runSchematic(options) {
309 const { schematicOptions, debug, dryRun } = options;
310 let { collectionName, schematicName } = options;
311 let nothingDone = true;
312 let loggingQueue = [];
313 let error = false;
314 const workflow = this._workflow;
315 const workingDir = core_1.normalize(systemPath.relative(this.context.root, process.cwd()));
316 // Get the option object from the schematic schema.
317 const schematic = this.getSchematic(this.getCollection(collectionName), schematicName, this.allowPrivateSchematics);
318 // Update the schematic and collection name in case they're not the same as the ones we
319 // received in our options, e.g. after alias resolution or extension.
320 collectionName = schematic.collection.description.name;
321 schematicName = schematic.description.name;
322 // Set the options of format "path".
323 let o = null;
324 let args;
325 if (!schematic.description.schemaJson) {
326 args = await this.parseFreeFormArguments(schematicOptions || []);
327 }
328 else {
329 o = await json_schema_1.parseJsonSchemaToOptions(workflow.registry, schematic.description.schemaJson);
330 args = await this.parseArguments(schematicOptions || [], o);
331 }
332 const allowAdditionalProperties = typeof schematic.description.schemaJson === 'object' &&
333 schematic.description.schemaJson.additionalProperties;
334 if (args['--'] && !allowAdditionalProperties) {
335 args['--'].forEach((additional) => {
336 this.logger.fatal(`Unknown option: '${additional.split(/=/)[0]}'`);
337 });
338 return 1;
339 }
340 const pathOptions = o ? this.setPathOptions(o, workingDir) : {};
341 const input = {
342 ...pathOptions,
343 ...args,
344 ...options.additionalOptions,
345 };
346 workflow.reporter.subscribe((event) => {
347 nothingDone = false;
348 // Strip leading slash to prevent confusion.
349 const eventPath = event.path.startsWith('/') ? event.path.substr(1) : event.path;
350 switch (event.kind) {
351 case 'error':
352 error = true;
353 const desc = event.description == 'alreadyExist' ? 'already exists' : 'does not exist.';
354 this.logger.warn(`ERROR! ${eventPath} ${desc}.`);
355 break;
356 case 'update':
357 loggingQueue.push(core_1.tags.oneLine `
358 ${color_1.colors.cyan('UPDATE')} ${eventPath} (${event.content.length} bytes)
359 `);
360 break;
361 case 'create':
362 loggingQueue.push(core_1.tags.oneLine `
363 ${color_1.colors.green('CREATE')} ${eventPath} (${event.content.length} bytes)
364 `);
365 break;
366 case 'delete':
367 loggingQueue.push(`${color_1.colors.yellow('DELETE')} ${eventPath}`);
368 break;
369 case 'rename':
370 const eventToPath = event.to.startsWith('/') ? event.to.substr(1) : event.to;
371 loggingQueue.push(`${color_1.colors.blue('RENAME')} ${eventPath} => ${eventToPath}`);
372 break;
373 }
374 });
375 workflow.lifeCycle.subscribe((event) => {
376 if (event.kind == 'end' || event.kind == 'post-tasks-start') {
377 if (!error) {
378 // Output the logging queue, no error happened.
379 loggingQueue.forEach((log) => this.logger.info(log));
380 }
381 loggingQueue = [];
382 error = false;
383 }
384 });
385 // Temporary compatibility check for NPM 7
386 if (collectionName === '@schematics/angular' && schematicName === 'ng-new') {
387 if (!input.skipInstall &&
388 (input.packageManager === undefined || input.packageManager === 'npm')) {
389 await package_manager_1.ensureCompatibleNpm(this.context.root);
390 }
391 }
392 return new Promise((resolve) => {
393 workflow
394 .execute({
395 collection: collectionName,
396 schematic: schematicName,
397 options: input,
398 debug: debug,
399 logger: this.logger,
400 allowPrivate: this.allowPrivateSchematics,
401 })
402 .subscribe({
403 error: (err) => {
404 // In case the workflow was not successful, show an appropriate error message.
405 if (err instanceof schematics_1.UnsuccessfulWorkflowExecution) {
406 // "See above" because we already printed the error.
407 this.logger.fatal('The Schematic workflow failed. See above.');
408 }
409 else if (debug) {
410 this.logger.fatal(`An error occurred:\n${err.message}\n${err.stack}`);
411 }
412 else {
413 this.logger.fatal(err.message);
414 }
415 resolve(1);
416 },
417 complete: () => {
418 const showNothingDone = !(options.showNothingDone === false);
419 if (nothingDone && showNothingDone) {
420 this.logger.info('Nothing to be done.');
421 }
422 if (dryRun) {
423 this.logger.warn(`\nNOTE: The "dryRun" flag means no changes were made.`);
424 }
425 resolve();
426 },
427 });
428 });
429 }
430 async parseFreeFormArguments(schematicOptions) {
431 return parser_1.parseFreeFormArguments(schematicOptions);
432 }
433 async parseArguments(schematicOptions, options) {
434 return parser_1.parseArguments(schematicOptions, options, this.logger);
435 }
436}
437exports.SchematicCommand = SchematicCommand;
438function getProjectsByPath(workspace, path, root) {
439 if (workspace.projects.size === 1) {
440 return Array.from(workspace.projects.keys());
441 }
442 const isInside = (base, potential) => {
443 const absoluteBase = systemPath.resolve(root, base);
444 const absolutePotential = systemPath.resolve(root, potential);
445 const relativePotential = systemPath.relative(absoluteBase, absolutePotential);
446 if (!relativePotential.startsWith('..') && !systemPath.isAbsolute(relativePotential)) {
447 return true;
448 }
449 return false;
450 };
451 const projects = Array.from(workspace.projects.entries())
452 .map(([name, project]) => [systemPath.resolve(root, project.root), name])
453 .filter((tuple) => isInside(tuple[0], path))
454 // Sort tuples by depth, with the deeper ones first. Since the first member is a path and
455 // we filtered all invalid paths, the longest will be the deepest (and in case of equality
456 // the sort is stable and the first declared project will win).
457 .sort((a, b) => b[0].length - a[0].length);
458 if (projects.length === 1) {
459 return [projects[0][1]];
460 }
461 else if (projects.length > 1) {
462 const firstPath = projects[0][0];
463 return projects.filter((v) => v[0] === firstPath).map((v) => v[1]);
464 }
465 return [];
466}