UNPKG

22.4 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.SchematicCommand = exports.UnknownCollectionError = void 0;
4/**
5 * @license
6 * Copyright Google Inc. All Rights Reserved.
7 *
8 * Use of this source code is governed by an MIT-style license that can be
9 * found in the LICENSE file at https://angular.io/license
10 */
11const core_1 = require("@angular-devkit/core");
12const node_1 = require("@angular-devkit/core/node");
13const schematics_1 = require("@angular-devkit/schematics");
14const tools_1 = require("@angular-devkit/schematics/tools");
15const inquirer = require("inquirer");
16const systemPath = require("path");
17const color_1 = require("../utilities/color");
18const config_1 = require("../utilities/config");
19const json_schema_1 = require("../utilities/json-schema");
20const package_manager_1 = require("../utilities/package-manager");
21const tty_1 = require("../utilities/tty");
22const analytics_1 = require("./analytics");
23const command_1 = require("./command");
24const parser_1 = require("./parser");
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._host = new node_1.NodeJsSyncHost();
36 this.defaultCollectionName = '@schematics/angular';
37 this.collectionName = this.defaultCollectionName;
38 }
39 async initialize(options) {
40 await this._loadWorkspace();
41 await this.createWorkflow(options);
42 if (this.schematicName) {
43 // Set the options.
44 const collection = this.getCollection(this.collectionName);
45 const schematic = this.getSchematic(collection, this.schematicName, true);
46 const options = await json_schema_1.parseJsonSchemaToOptions(this._workflow.registry, schematic.description.schemaJson || {});
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 else if (schematicNames.length == 1) {
88 this.logger.info('Help for schematic ' + schematicNames[0]);
89 await this.printHelpSubcommand(subCommandOption.subcommands[schematicNames[0]]);
90 }
91 return 0;
92 }
93 async printHelpUsage() {
94 const subCommandOption = this.description.options.filter(x => x.subcommands)[0];
95 if (!subCommandOption || !subCommandOption.subcommands) {
96 return;
97 }
98 const schematicNames = Object.keys(subCommandOption.subcommands);
99 if (schematicNames.length == 1) {
100 this.logger.info(this.description.description);
101 const opts = this.description.options.filter(x => x.positional === undefined);
102 const [collectionName, schematicName] = schematicNames[0].split(/:/)[0];
103 // Display <collectionName:schematicName> if this is not the default collectionName,
104 // otherwise just show the schematicName.
105 const displayName = collectionName == (await this.getDefaultSchematicCollection())
106 ? schematicName
107 : schematicNames[0];
108 const schematicOptions = subCommandOption.subcommands[schematicNames[0]].options;
109 const schematicArgs = schematicOptions.filter(x => x.positional !== undefined);
110 const argDisplay = schematicArgs.length > 0
111 ? ' ' + schematicArgs.map(a => `<${core_1.strings.dasherize(a.name)}>`).join(' ')
112 : '';
113 this.logger.info(core_1.tags.oneLine `
114 usage: ng ${this.description.name} ${displayName}${argDisplay}
115 ${opts.length > 0 ? `[options]` : ``}
116 `);
117 this.logger.info('');
118 }
119 else {
120 await super.printHelpUsage();
121 }
122 }
123 getEngine() {
124 return this._workflow.engine;
125 }
126 getCollection(collectionName) {
127 const engine = this.getEngine();
128 const collection = engine.createCollection(collectionName);
129 if (collection === null) {
130 throw new UnknownCollectionError(collectionName);
131 }
132 return collection;
133 }
134 getSchematic(collection, schematicName, allowPrivate) {
135 return collection.createSchematic(schematicName, allowPrivate);
136 }
137 setPathOptions(options, workingDir) {
138 if (workingDir === '') {
139 return {};
140 }
141 return options
142 .filter(o => o.format === 'path')
143 .map(o => o.name)
144 .reduce((acc, curr) => {
145 acc[curr] = workingDir;
146 return acc;
147 }, {});
148 }
149 /*
150 * Runtime hook to allow specifying customized workflow
151 */
152 async createWorkflow(options) {
153 if (this._workflow) {
154 return this._workflow;
155 }
156 const { force, dryRun } = options;
157 const fsHost = new core_1.virtualFs.ScopedHost(new node_1.NodeJsSyncHost(), core_1.normalize(this.workspace.root));
158 const workflow = new tools_1.NodeWorkflow(fsHost, {
159 force,
160 dryRun,
161 packageManager: await package_manager_1.getPackageManager(this.workspace.root),
162 packageRegistry: options.packageRegistry,
163 root: core_1.normalize(this.workspace.root),
164 registry: new core_1.schema.CoreSchemaRegistry(schematics_1.formats.standardFormats),
165 resolvePaths: !!this.workspace.configFile
166 // Workspace
167 ? this.collectionName === this.defaultCollectionName
168 // Favor __dirname for @schematics/angular to use the build-in version
169 ? [__dirname, process.cwd(), this.workspace.root]
170 : [process.cwd(), this.workspace.root, __dirname]
171 // Global
172 : [__dirname, process.cwd()],
173 });
174 workflow.engineHost.registerContextTransform(context => {
175 // This is run by ALL schematics, so if someone uses `externalSchematics(...)` which
176 // is safelisted, it would move to the right analytics (even if their own isn't).
177 const collectionName = context.schematic.collection.description.name;
178 if (analytics_1.isPackageNameSafeForAnalytics(collectionName)) {
179 return {
180 ...context,
181 analytics: this.analytics,
182 };
183 }
184 else {
185 return context;
186 }
187 });
188 const getProjectName = () => {
189 if (this._workspace) {
190 const projectNames = getProjectsByPath(this._workspace, process.cwd(), this.workspace.root);
191 if (projectNames.length === 1) {
192 return projectNames[0];
193 }
194 else {
195 if (projectNames.length > 1) {
196 this.logger.warn(core_1.tags.oneLine `
197 Two or more projects are using identical roots.
198 Unable to determine project using current working directory.
199 Using default workspace project instead.
200 `);
201 }
202 const defaultProjectName = this._workspace.extensions['defaultProject'];
203 if (typeof defaultProjectName === 'string' && defaultProjectName) {
204 return defaultProjectName;
205 }
206 }
207 }
208 return undefined;
209 };
210 const defaultOptionTransform = async (schematic, current) => ({
211 ...(await config_1.getSchematicDefaults(schematic.collection.name, schematic.name, getProjectName())),
212 ...current,
213 });
214 workflow.engineHost.registerOptionsTransform(defaultOptionTransform);
215 if (options.defaults) {
216 workflow.registry.addPreTransform(core_1.schema.transforms.addUndefinedDefaults);
217 }
218 else {
219 workflow.registry.addPostTransform(core_1.schema.transforms.addUndefinedDefaults);
220 }
221 workflow.engineHost.registerOptionsTransform(tools_1.validateOptionsWithSchema(workflow.registry));
222 workflow.registry.addSmartDefaultProvider('projectName', getProjectName);
223 workflow.registry.useXDeprecatedProvider(msg => this.logger.warn(msg));
224 if (options.interactive !== false && tty_1.isTTY()) {
225 workflow.registry.usePromptProvider((definitions) => {
226 const questions = definitions.map(definition => {
227 var _a;
228 const question = {
229 name: definition.id,
230 message: definition.message,
231 default: definition.default,
232 };
233 const validator = definition.validator;
234 if (validator) {
235 question.validate = input => validator(input);
236 }
237 switch (definition.type) {
238 case 'confirmation':
239 question.type = 'confirm';
240 break;
241 case 'list':
242 question.type = definition.multiselect ? 'checkbox' : 'list';
243 question.choices = (_a = definition.items) === null || _a === void 0 ? void 0 : _a.map(item => {
244 return typeof item == 'string'
245 ? item
246 : {
247 name: item.label,
248 value: item.value,
249 };
250 });
251 break;
252 default:
253 question.type = definition.type;
254 break;
255 }
256 return question;
257 });
258 return inquirer.prompt(questions);
259 });
260 }
261 return (this._workflow = workflow);
262 }
263 async getDefaultSchematicCollection() {
264 let workspace = await config_1.getWorkspace('local');
265 if (workspace) {
266 const project = config_1.getProjectByCwd(workspace);
267 if (project && workspace.getProjectCli(project)) {
268 const value = workspace.getProjectCli(project)['defaultCollection'];
269 if (typeof value == 'string') {
270 return value;
271 }
272 }
273 if (workspace.getCli()) {
274 const value = workspace.getCli()['defaultCollection'];
275 if (typeof value == 'string') {
276 return value;
277 }
278 }
279 }
280 workspace = await config_1.getWorkspace('global');
281 if (workspace && workspace.getCli()) {
282 const value = workspace.getCli()['defaultCollection'];
283 if (typeof value == 'string') {
284 return value;
285 }
286 }
287 return this.defaultCollectionName;
288 }
289 async runSchematic(options) {
290 const { schematicOptions, debug, dryRun } = options;
291 let { collectionName, schematicName } = options;
292 let nothingDone = true;
293 let loggingQueue = [];
294 let error = false;
295 const workflow = this._workflow;
296 const workingDir = core_1.normalize(systemPath.relative(this.workspace.root, process.cwd()));
297 // Get the option object from the schematic schema.
298 const schematic = this.getSchematic(this.getCollection(collectionName), schematicName, this.allowPrivateSchematics);
299 // Update the schematic and collection name in case they're not the same as the ones we
300 // received in our options, e.g. after alias resolution or extension.
301 collectionName = schematic.collection.description.name;
302 schematicName = schematic.description.name;
303 // TODO: Remove warning check when 'targets' is default
304 if (collectionName !== this.defaultCollectionName) {
305 const [ast, configPath] = config_1.getWorkspaceRaw('local');
306 if (ast) {
307 const projectsKeyValue = ast.properties.find(p => p.key.value === 'projects');
308 if (!projectsKeyValue || projectsKeyValue.value.kind !== 'object') {
309 return;
310 }
311 const positions = [];
312 for (const projectKeyValue of projectsKeyValue.value.properties) {
313 const projectNode = projectKeyValue.value;
314 if (projectNode.kind !== 'object') {
315 continue;
316 }
317 const targetsKeyValue = projectNode.properties.find(p => p.key.value === 'targets');
318 if (targetsKeyValue) {
319 positions.push(targetsKeyValue.start);
320 }
321 }
322 if (positions.length > 0) {
323 const warning = core_1.tags.oneLine `
324 WARNING: This command may not execute successfully.
325 The package/collection may not support the 'targets' field within '${configPath}'.
326 This can be corrected by renaming the following 'targets' fields to 'architect':
327 `;
328 const locations = positions
329 .map((p, i) => `${i + 1}) Line: ${p.line + 1}; Column: ${p.character + 1}`)
330 .join('\n');
331 this.logger.warn(warning + '\n' + locations + '\n');
332 }
333 }
334 }
335 // Set the options of format "path".
336 let o = null;
337 let args;
338 if (!schematic.description.schemaJson) {
339 args = await this.parseFreeFormArguments(schematicOptions || []);
340 }
341 else {
342 o = await json_schema_1.parseJsonSchemaToOptions(workflow.registry, schematic.description.schemaJson);
343 args = await this.parseArguments(schematicOptions || [], o);
344 }
345 const allowAdditionalProperties = typeof schematic.description.schemaJson === 'object' && schematic.description.schemaJson.additionalProperties;
346 if (args['--'] && !allowAdditionalProperties) {
347 args['--'].forEach(additional => {
348 this.logger.fatal(`Unknown option: '${additional.split(/=/)[0]}'`);
349 });
350 return 1;
351 }
352 const pathOptions = o ? this.setPathOptions(o, workingDir) : {};
353 let input = { ...pathOptions, ...args };
354 // Read the default values from the workspace.
355 const projectName = input.project !== undefined ? '' + input.project : null;
356 const defaults = await config_1.getSchematicDefaults(collectionName, schematicName, projectName);
357 input = {
358 ...defaults,
359 ...input,
360 ...options.additionalOptions,
361 };
362 workflow.reporter.subscribe((event) => {
363 nothingDone = false;
364 // Strip leading slash to prevent confusion.
365 const eventPath = event.path.startsWith('/') ? event.path.substr(1) : event.path;
366 switch (event.kind) {
367 case 'error':
368 error = true;
369 const desc = event.description == 'alreadyExist' ? 'already exists' : 'does not exist.';
370 this.logger.warn(`ERROR! ${eventPath} ${desc}.`);
371 break;
372 case 'update':
373 loggingQueue.push(core_1.tags.oneLine `
374 ${color_1.colors.white('UPDATE')} ${eventPath} (${event.content.length} bytes)
375 `);
376 break;
377 case 'create':
378 loggingQueue.push(core_1.tags.oneLine `
379 ${color_1.colors.green('CREATE')} ${eventPath} (${event.content.length} bytes)
380 `);
381 break;
382 case 'delete':
383 loggingQueue.push(`${color_1.colors.yellow('DELETE')} ${eventPath}`);
384 break;
385 case 'rename':
386 const eventToPath = event.to.startsWith('/') ? event.to.substr(1) : event.to;
387 loggingQueue.push(`${color_1.colors.blue('RENAME')} ${eventPath} => ${eventToPath}`);
388 break;
389 }
390 });
391 workflow.lifeCycle.subscribe(event => {
392 if (event.kind == 'end' || event.kind == 'post-tasks-start') {
393 if (!error) {
394 // Output the logging queue, no error happened.
395 loggingQueue.forEach(log => this.logger.info(log));
396 }
397 loggingQueue = [];
398 error = false;
399 }
400 });
401 return new Promise(resolve => {
402 workflow
403 .execute({
404 collection: collectionName,
405 schematic: schematicName,
406 options: input,
407 debug: debug,
408 logger: this.logger,
409 allowPrivate: this.allowPrivateSchematics,
410 })
411 .subscribe({
412 error: (err) => {
413 // In case the workflow was not successful, show an appropriate error message.
414 if (err instanceof schematics_1.UnsuccessfulWorkflowExecution) {
415 // "See above" because we already printed the error.
416 this.logger.fatal('The Schematic workflow failed. See above.');
417 }
418 else if (debug) {
419 this.logger.fatal(`An error occured:\n${err.message}\n${err.stack}`);
420 }
421 else {
422 this.logger.fatal(err.message);
423 }
424 resolve(1);
425 },
426 complete: () => {
427 const showNothingDone = !(options.showNothingDone === false);
428 if (nothingDone && showNothingDone) {
429 this.logger.info('Nothing to be done.');
430 }
431 if (dryRun) {
432 this.logger.warn(`\nNOTE: The "dryRun" flag means no changes were made.`);
433 }
434 resolve();
435 },
436 });
437 });
438 }
439 async parseFreeFormArguments(schematicOptions) {
440 return parser_1.parseFreeFormArguments(schematicOptions);
441 }
442 async parseArguments(schematicOptions, options) {
443 return parser_1.parseArguments(schematicOptions, options, this.logger);
444 }
445 async _loadWorkspace() {
446 if (this._workspace) {
447 return;
448 }
449 try {
450 const { workspace } = await core_1.workspaces.readWorkspace(this.workspace.root, core_1.workspaces.createWorkspaceHost(this._host));
451 this._workspace = workspace;
452 }
453 catch (err) {
454 if (!this.allowMissingWorkspace) {
455 // Ignore missing workspace
456 throw err;
457 }
458 }
459 }
460}
461exports.SchematicCommand = SchematicCommand;
462function getProjectsByPath(workspace, path, root) {
463 if (workspace.projects.size === 1) {
464 return Array.from(workspace.projects.keys());
465 }
466 const isInside = (base, potential) => {
467 const absoluteBase = systemPath.resolve(root, base);
468 const absolutePotential = systemPath.resolve(root, potential);
469 const relativePotential = systemPath.relative(absoluteBase, absolutePotential);
470 if (!relativePotential.startsWith('..') && !systemPath.isAbsolute(relativePotential)) {
471 return true;
472 }
473 return false;
474 };
475 const projects = Array.from(workspace.projects.entries())
476 .map(([name, project]) => [systemPath.resolve(root, project.root), name])
477 .filter(tuple => isInside(tuple[0], path))
478 // Sort tuples by depth, with the deeper ones first. Since the first member is a path and
479 // we filtered all invalid paths, the longest will be the deepest (and in case of equality
480 // the sort is stable and the first declared project will win).
481 .sort((a, b) => b[0].length - a[0].length);
482 if (projects.length === 1) {
483 return [projects[0][1]];
484 }
485 else if (projects.length > 1) {
486 const firstPath = projects[0][0];
487 return projects.filter(v => v[0] === firstPath).map(v => v[1]);
488 }
489 return [];
490}