UNPKG

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