architect-command.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  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 architect_1 = require("@angular-devkit/architect");
  11. const node_1 = require("@angular-devkit/architect/node");
  12. const core_1 = require("@angular-devkit/core");
  13. const node_2 = require("@angular-devkit/core/node");
  14. const bep_1 = require("../utilities/bep");
  15. const json_schema_1 = require("../utilities/json-schema");
  16. const analytics_1 = require("./analytics");
  17. const command_1 = require("./command");
  18. const parser_1 = require("./parser");
  19. class ArchitectCommand extends command_1.Command {
  20. constructor() {
  21. super(...arguments);
  22. // If this command supports running multiple targets.
  23. this.multiTarget = false;
  24. }
  25. async initialize(options) {
  26. await super.initialize(options);
  27. this._registry = new core_1.json.schema.CoreSchemaRegistry();
  28. this._registry.addPostTransform(core_1.json.schema.transforms.addUndefinedDefaults);
  29. const { workspace } = await core_1.workspaces.readWorkspace(this.workspace.root, core_1.workspaces.createWorkspaceHost(new node_2.NodeJsSyncHost()));
  30. this._workspace = workspace;
  31. this._architectHost = new node_1.WorkspaceNodeModulesArchitectHost(workspace, this.workspace.root);
  32. this._architect = new architect_1.Architect(this._architectHost, this._registry);
  33. if (!this.target) {
  34. if (options.help) {
  35. // This is a special case where we just return.
  36. return;
  37. }
  38. const specifier = this._makeTargetSpecifier(options);
  39. if (!specifier.project || !specifier.target) {
  40. throw new Error('Cannot determine project or target for command.');
  41. }
  42. return;
  43. }
  44. const commandLeftovers = options['--'];
  45. let projectName = options.project;
  46. const targetProjectNames = [];
  47. for (const [name, project] of this._workspace.projects) {
  48. if (project.targets.has(this.target)) {
  49. targetProjectNames.push(name);
  50. }
  51. }
  52. if (targetProjectNames.length === 0) {
  53. throw new Error(this.missingTargetError || `No projects support the '${this.target}' target.`);
  54. }
  55. if (projectName && !targetProjectNames.includes(projectName)) {
  56. throw new Error(this.missingTargetError ||
  57. `Project '${projectName}' does not support the '${this.target}' target.`);
  58. }
  59. if (!projectName && commandLeftovers && commandLeftovers.length > 0) {
  60. const builderNames = new Set();
  61. const leftoverMap = new Map();
  62. let potentialProjectNames = new Set(targetProjectNames);
  63. for (const name of targetProjectNames) {
  64. const builderName = await this._architectHost.getBuilderNameForTarget({
  65. project: name,
  66. target: this.target,
  67. });
  68. if (this.multiTarget) {
  69. builderNames.add(builderName);
  70. }
  71. const builderDesc = await this._architectHost.resolveBuilder(builderName);
  72. const optionDefs = await json_schema_1.parseJsonSchemaToOptions(this._registry, builderDesc.optionSchema);
  73. const parsedOptions = parser_1.parseArguments([...commandLeftovers], optionDefs);
  74. const builderLeftovers = parsedOptions['--'] || [];
  75. leftoverMap.set(name, { optionDefs, parsedOptions });
  76. potentialProjectNames = new Set(builderLeftovers.filter(x => potentialProjectNames.has(x)));
  77. }
  78. if (potentialProjectNames.size === 1) {
  79. projectName = [...potentialProjectNames][0];
  80. // remove the project name from the leftovers
  81. const optionInfo = leftoverMap.get(projectName);
  82. if (optionInfo) {
  83. const locations = [];
  84. let i = 0;
  85. while (i < commandLeftovers.length) {
  86. i = commandLeftovers.indexOf(projectName, i + 1);
  87. if (i === -1) {
  88. break;
  89. }
  90. locations.push(i);
  91. }
  92. delete optionInfo.parsedOptions['--'];
  93. for (const location of locations) {
  94. const tempLeftovers = [...commandLeftovers];
  95. tempLeftovers.splice(location, 1);
  96. const tempArgs = parser_1.parseArguments([...tempLeftovers], optionInfo.optionDefs);
  97. delete tempArgs['--'];
  98. if (JSON.stringify(optionInfo.parsedOptions) === JSON.stringify(tempArgs)) {
  99. options['--'] = tempLeftovers;
  100. break;
  101. }
  102. }
  103. }
  104. }
  105. if (!projectName && this.multiTarget && builderNames.size > 1) {
  106. throw new Error(core_1.tags.oneLine `
  107. Architect commands with command line overrides cannot target different builders. The
  108. '${this.target}' target would run on projects ${targetProjectNames.join()} which have the
  109. following builders: ${'\n ' + [...builderNames].join('\n ')}
  110. `);
  111. }
  112. }
  113. if (!projectName && !this.multiTarget) {
  114. const defaultProjectName = this._workspace.extensions['defaultProject'];
  115. if (targetProjectNames.length === 1) {
  116. projectName = targetProjectNames[0];
  117. }
  118. else if (defaultProjectName && targetProjectNames.includes(defaultProjectName)) {
  119. projectName = defaultProjectName;
  120. }
  121. else if (options.help) {
  122. // This is a special case where we just return.
  123. return;
  124. }
  125. else {
  126. throw new Error(this.missingTargetError || 'Cannot determine project or target for command.');
  127. }
  128. }
  129. options.project = projectName;
  130. const builderConf = await this._architectHost.getBuilderNameForTarget({
  131. project: projectName || (targetProjectNames.length > 0 ? targetProjectNames[0] : ''),
  132. target: this.target,
  133. });
  134. const builderDesc = await this._architectHost.resolveBuilder(builderConf);
  135. this.description.options.push(...(await json_schema_1.parseJsonSchemaToOptions(this._registry, builderDesc.optionSchema)));
  136. // Update options to remove analytics from options if the builder isn't safelisted.
  137. for (const o of this.description.options) {
  138. if (o.userAnalytics) {
  139. if (!analytics_1.isPackageNameSafeForAnalytics(builderConf)) {
  140. o.userAnalytics = undefined;
  141. }
  142. }
  143. }
  144. }
  145. async run(options) {
  146. return await this.runArchitectTarget(options);
  147. }
  148. async runBepTarget(command, configuration, overrides, buildEventLog) {
  149. const bep = new bep_1.BepJsonWriter(buildEventLog);
  150. // Send start
  151. bep.writeBuildStarted(command);
  152. let last = 1;
  153. let rebuild = false;
  154. const run = await this._architect.scheduleTarget(configuration, overrides, {
  155. logger: this.logger,
  156. });
  157. await run.output.forEach(event => {
  158. last = event.success ? 0 : 1;
  159. if (rebuild) {
  160. // NOTE: This will have an incorrect timestamp but this cannot be fixed
  161. // until builders report additional status events
  162. bep.writeBuildStarted(command);
  163. }
  164. else {
  165. rebuild = true;
  166. }
  167. bep.writeBuildFinished(last);
  168. });
  169. await run.stop();
  170. return last;
  171. }
  172. async runSingleTarget(target, targetOptions, commandOptions) {
  173. // We need to build the builderSpec twice because architect does not understand
  174. // overrides separately (getting the configuration builds the whole project, including
  175. // overrides).
  176. const builderConf = await this._architectHost.getBuilderNameForTarget(target);
  177. const builderDesc = await this._architectHost.resolveBuilder(builderConf);
  178. const targetOptionArray = await json_schema_1.parseJsonSchemaToOptions(this._registry, builderDesc.optionSchema);
  179. const overrides = parser_1.parseArguments(targetOptions, targetOptionArray, this.logger);
  180. const allowAdditionalProperties = typeof builderDesc.optionSchema === 'object' && builderDesc.optionSchema.additionalProperties;
  181. if (overrides['--'] && !allowAdditionalProperties) {
  182. (overrides['--'] || []).forEach(additional => {
  183. this.logger.fatal(`Unknown option: '${additional.split(/=/)[0]}'`);
  184. });
  185. return 1;
  186. }
  187. if (commandOptions.buildEventLog && ['build', 'serve'].includes(this.description.name)) {
  188. // The build/serve commands supports BEP messaging
  189. this.logger.warn('BEP support is experimental and subject to change.');
  190. return this.runBepTarget(this.description.name, target, overrides, commandOptions.buildEventLog);
  191. }
  192. else {
  193. const run = await this._architect.scheduleTarget(target, overrides, {
  194. logger: this.logger,
  195. analytics: analytics_1.isPackageNameSafeForAnalytics(builderConf) ? this.analytics : undefined,
  196. });
  197. const { error, success } = await run.output.toPromise();
  198. await run.stop();
  199. if (error) {
  200. this.logger.error(error);
  201. }
  202. return success ? 0 : 1;
  203. }
  204. }
  205. async runArchitectTarget(options) {
  206. const extra = options['--'] || [];
  207. try {
  208. const targetSpec = this._makeTargetSpecifier(options);
  209. if (!targetSpec.project && this.target) {
  210. // This runs each target sequentially.
  211. // Running them in parallel would jumble the log messages.
  212. let result = 0;
  213. for (const project of this.getProjectNamesByTarget(this.target)) {
  214. result |= await this.runSingleTarget({ ...targetSpec, project }, extra, options);
  215. }
  216. return result;
  217. }
  218. else {
  219. return await this.runSingleTarget(targetSpec, extra, options);
  220. }
  221. }
  222. catch (e) {
  223. if (e instanceof core_1.schema.SchemaValidationException) {
  224. const newErrors = [];
  225. for (const schemaError of e.errors) {
  226. if (schemaError.keyword === 'additionalProperties') {
  227. const unknownProperty = schemaError.params.additionalProperty;
  228. if (unknownProperty in options) {
  229. const dashes = unknownProperty.length === 1 ? '-' : '--';
  230. this.logger.fatal(`Unknown option: '${dashes}${unknownProperty}'`);
  231. continue;
  232. }
  233. }
  234. newErrors.push(schemaError);
  235. }
  236. if (newErrors.length > 0) {
  237. this.logger.error(new core_1.schema.SchemaValidationException(newErrors).message);
  238. }
  239. return 1;
  240. }
  241. else {
  242. throw e;
  243. }
  244. }
  245. }
  246. getProjectNamesByTarget(targetName) {
  247. const allProjectsForTargetName = [];
  248. for (const [name, project] of this._workspace.projects) {
  249. if (project.targets.has(targetName)) {
  250. allProjectsForTargetName.push(name);
  251. }
  252. }
  253. if (this.multiTarget) {
  254. // For multi target commands, we always list all projects that have the target.
  255. return allProjectsForTargetName;
  256. }
  257. else {
  258. // For single target commands, we try the default project first,
  259. // then the full list if it has a single project, then error out.
  260. const maybeDefaultProject = this._workspace.extensions['defaultProject'];
  261. if (maybeDefaultProject && allProjectsForTargetName.includes(maybeDefaultProject)) {
  262. return [maybeDefaultProject];
  263. }
  264. if (allProjectsForTargetName.length === 1) {
  265. return allProjectsForTargetName;
  266. }
  267. throw new Error(`Could not determine a single project for the '${targetName}' target.`);
  268. }
  269. }
  270. _makeTargetSpecifier(commandOptions) {
  271. let project, target, configuration;
  272. if (commandOptions.target) {
  273. [project, target, configuration] = commandOptions.target.split(':');
  274. if (commandOptions.configuration) {
  275. configuration = commandOptions.configuration;
  276. }
  277. }
  278. else {
  279. project = commandOptions.project;
  280. target = this.target;
  281. configuration = commandOptions.configuration;
  282. if (!configuration && commandOptions.prod) {
  283. configuration = 'production';
  284. }
  285. }
  286. if (!project) {
  287. project = '';
  288. }
  289. if (!target) {
  290. target = '';
  291. }
  292. return {
  293. project,
  294. configuration: configuration || '',
  295. target,
  296. };
  297. }
  298. }
  299. exports.ArchitectCommand = ArchitectCommand;