UNPKG

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