Skip to content

Commit 5e7f995

Browse files
clydinhansl
authored andcommitted
fix(@angular/cli): improve architect command project parsing
1 parent c631c18 commit 5e7f995

File tree

2 files changed

+105
-62
lines changed

2 files changed

+105
-62
lines changed

packages/angular/cli/models/architect-command.ts

+100-62
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,13 @@
77
*/
88
import {
99
Architect,
10-
BuilderConfiguration,
1110
TargetSpecifier,
1211
} from '@angular-devkit/architect';
1312
import { experimental, json, schema, tags } from '@angular-devkit/core';
1413
import { NodeJsSyncHost, createConsoleLogger } from '@angular-devkit/core/node';
1514
import { parseJsonSchemaToOptions } from '../utilities/json-schema';
1615
import { BaseCommandOptions, Command } from './command';
17-
import { Arguments } from './interface';
16+
import { Arguments, Option } from './interface';
1817
import { parseArguments } from './parser';
1918
import { WorkspaceLoader } from './workspace-loader';
2019

@@ -46,84 +45,123 @@ export abstract class ArchitectCommand<
4645

4746
await this._loadWorkspaceAndArchitect();
4847

49-
if (!options.project && this.target) {
50-
const projectNames = this.getProjectNamesByTarget(this.target);
51-
const leftovers = options['--'];
52-
if (projectNames.length > 1 && leftovers && leftovers.length > 0) {
53-
// Verify that all builders are the same, otherwise error out (since the meaning of an
54-
// option could vary from builder to builder).
55-
56-
const builders: string[] = [];
57-
for (const projectName of projectNames) {
58-
const targetSpec: TargetSpecifier = this._makeTargetSpecifier(options);
59-
const targetDesc = this._architect.getBuilderConfiguration({
60-
project: projectName,
61-
target: targetSpec.target,
62-
});
63-
64-
if (builders.indexOf(targetDesc.builder) == -1) {
65-
builders.push(targetDesc.builder);
66-
}
67-
}
68-
69-
if (builders.length > 1) {
70-
throw new Error(tags.oneLine`
71-
Architect commands with command line overrides cannot target different builders. The
72-
'${this.target}' target would run on projects ${projectNames.join()} which have the
73-
following builders: ${'\n ' + builders.join('\n ')}
74-
`);
75-
}
48+
if (!this.target) {
49+
if (options.help) {
50+
// This is a special case where we just return.
51+
return;
7652
}
77-
}
7853

79-
const targetSpec: TargetSpecifier = this._makeTargetSpecifier(options);
54+
const specifier = this._makeTargetSpecifier(options);
55+
if (!specifier.project || !specifier.target) {
56+
throw new Error('Cannot determine project or target for command.');
57+
}
8058

81-
if (this.target && !targetSpec.project) {
82-
const projects = this.getProjectNamesByTarget(this.target);
59+
return;
60+
}
8361

84-
if (projects.length === 1) {
85-
// If there is a single target, use it to parse overrides.
86-
targetSpec.project = projects[0];
62+
const commandLeftovers = options['--'];
63+
let projectName = options.project;
64+
const targetProjectNames: string[] = [];
65+
for (const name of this._workspace.listProjectNames()) {
66+
if (this._architect.listProjectTargets(name).includes(this.target)) {
67+
targetProjectNames.push(name);
8768
}
8869
}
8970

90-
if ((!targetSpec.project || !targetSpec.target) && !this.multiTarget) {
91-
if (options.help) {
92-
// This is a special case where we just return.
93-
return;
94-
}
71+
if (targetProjectNames.length === 0) {
72+
throw new Error(`No projects support the '${this.target}' target.`);
73+
}
9574

96-
throw new Error('Cannot determine project or target for Architect command.');
75+
if (projectName && !targetProjectNames.includes(projectName)) {
76+
throw new Error(`Project '${projectName}' does not support the '${this.target}' target.`);
9777
}
9878

99-
if (this.target) {
100-
// Add options IF there's only one builder of this kind.
101-
const targetSpec: TargetSpecifier = this._makeTargetSpecifier(options);
102-
const projectNames = targetSpec.project
103-
? [targetSpec.project]
104-
: this.getProjectNamesByTarget(this.target);
105-
106-
const builderConfigurations: BuilderConfiguration[] = [];
107-
for (const projectName of projectNames) {
108-
const targetDesc = this._architect.getBuilderConfiguration({
109-
project: projectName,
110-
target: targetSpec.target,
79+
if (!projectName && commandLeftovers && commandLeftovers.length > 0) {
80+
const builderNames = new Set<string>();
81+
const leftoverMap = new Map<string, { optionDefs: Option[], parsedOptions: Arguments }>();
82+
let potentialProjectNames = new Set<string>(targetProjectNames);
83+
for (const name of targetProjectNames) {
84+
const builderConfig = this._architect.getBuilderConfiguration({
85+
project: name,
86+
target: this.target,
11187
});
11288

113-
if (!builderConfigurations.find(b => b.builder === targetDesc.builder)) {
114-
builderConfigurations.push(targetDesc);
89+
if (this.multiTarget) {
90+
builderNames.add(builderConfig.builder);
11591
}
92+
93+
const builderDesc = await this._architect.getBuilderDescription(builderConfig).toPromise();
94+
const optionDefs = await parseJsonSchemaToOptions(this._registry, builderDesc.schema);
95+
const parsedOptions = parseArguments([...commandLeftovers], optionDefs);
96+
const builderLeftovers = parsedOptions['--'] || [];
97+
leftoverMap.set(name, { optionDefs, parsedOptions });
98+
99+
potentialProjectNames = new Set(builderLeftovers.filter(x => potentialProjectNames.has(x)));
116100
}
117101

118-
if (builderConfigurations.length == 1) {
119-
const builderConf = builderConfigurations[0];
120-
const builderDesc = await this._architect.getBuilderDescription(builderConf).toPromise();
102+
if (potentialProjectNames.size === 1) {
103+
projectName = [...potentialProjectNames][0];
104+
105+
// remove the project name from the leftovers
106+
const optionInfo = leftoverMap.get(projectName);
107+
if (optionInfo) {
108+
const locations = [];
109+
let i = 0;
110+
while (i < commandLeftovers.length) {
111+
i = commandLeftovers.indexOf(projectName, i + 1);
112+
if (i === -1) {
113+
break;
114+
}
115+
locations.push(i);
116+
}
117+
delete optionInfo.parsedOptions['--'];
118+
for (const location of locations) {
119+
const tempLeftovers = [...commandLeftovers];
120+
tempLeftovers.splice(location, 1);
121+
const tempArgs = parseArguments([...tempLeftovers], optionInfo.optionDefs);
122+
delete tempArgs['--'];
123+
if (JSON.stringify(optionInfo.parsedOptions) === JSON.stringify(tempArgs)) {
124+
options['--'] = tempLeftovers;
125+
break;
126+
}
127+
}
128+
}
129+
}
130+
131+
if (!projectName && this.multiTarget && builderNames.size > 1) {
132+
throw new Error(tags.oneLine`
133+
Architect commands with command line overrides cannot target different builders. The
134+
'${this.target}' target would run on projects ${targetProjectNames.join()} which have the
135+
following builders: ${'\n ' + [...builderNames].join('\n ')}
136+
`);
137+
}
138+
}
121139

122-
this.description.options.push(...(
123-
await parseJsonSchemaToOptions(this._registry, builderDesc.schema)
124-
));
140+
if (!projectName && !this.multiTarget) {
141+
const defaultProjectName = this._workspace.getDefaultProjectName();
142+
if (targetProjectNames.length === 1) {
143+
projectName = targetProjectNames[0];
144+
} else if (defaultProjectName && targetProjectNames.includes(defaultProjectName)) {
145+
projectName = defaultProjectName;
146+
} else if (options.help) {
147+
// This is a special case where we just return.
148+
return;
149+
} else {
150+
throw new Error('Cannot determine project or target for command.');
125151
}
126152
}
153+
154+
options.project = projectName;
155+
156+
const builderConf = this._architect.getBuilderConfiguration({
157+
project: projectName || (targetProjectNames.length > 0 ? targetProjectNames[0] : ''),
158+
target: this.target,
159+
});
160+
const builderDesc = await this._architect.getBuilderDescription(builderConf).toPromise();
161+
162+
this.description.options.push(...(
163+
await parseJsonSchemaToOptions(this._registry, builderDesc.schema)
164+
));
127165
}
128166

129167
async run(options: ArchitectCommandOptions & Arguments) {

tests/legacy-cli/e2e/tests/basic/build.ts

+5
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,15 @@ export default async function() {
77
await ng('build');
88
await expectFileToMatch('dist/test-project/index.html', 'main.js');
99

10+
// Named Development build
11+
await ng('build', 'test-project');
12+
await ng('build', 'test-project', '--no-progress');
13+
await ng('build', '--no-progress', 'test-project');
1014

1115
// Production build
1216
await ng('build', '--prod');
1317
await expectFileToMatch('dist/test-project/index.html', /main\.[a-zA-Z0-9]{20}\.js/);
18+
await ng('build', '--prod', '--no-progress', 'test-project');
1419

1520
// Store the production build for artifact storage on CircleCI
1621
if (process.env['CIRCLECI']) {

0 commit comments

Comments
 (0)