schematic-command.js 21 KB

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