Skip to content

Commit

Permalink
fix(@angular/cli): improve architect command project parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
clydin authored and hansl committed Nov 16, 2018
1 parent c631c18 commit 5e7f995
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 62 deletions.
162 changes: 100 additions & 62 deletions packages/angular/cli/models/architect-command.ts
Expand Up @@ -7,14 +7,13 @@
*/
import {
Architect,
BuilderConfiguration,
TargetSpecifier,
} from '@angular-devkit/architect';
import { experimental, json, schema, tags } from '@angular-devkit/core';
import { NodeJsSyncHost, createConsoleLogger } from '@angular-devkit/core/node';
import { parseJsonSchemaToOptions } from '../utilities/json-schema';
import { BaseCommandOptions, Command } from './command';
import { Arguments } from './interface';
import { Arguments, Option } from './interface';
import { parseArguments } from './parser';
import { WorkspaceLoader } from './workspace-loader';

Expand Down Expand Up @@ -46,84 +45,123 @@ export abstract class ArchitectCommand<

await this._loadWorkspaceAndArchitect();

if (!options.project && this.target) {
const projectNames = this.getProjectNamesByTarget(this.target);
const leftovers = options['--'];
if (projectNames.length > 1 && leftovers && leftovers.length > 0) {
// Verify that all builders are the same, otherwise error out (since the meaning of an
// option could vary from builder to builder).

const builders: string[] = [];
for (const projectName of projectNames) {
const targetSpec: TargetSpecifier = this._makeTargetSpecifier(options);
const targetDesc = this._architect.getBuilderConfiguration({
project: projectName,
target: targetSpec.target,
});

if (builders.indexOf(targetDesc.builder) == -1) {
builders.push(targetDesc.builder);
}
}

if (builders.length > 1) {
throw new Error(tags.oneLine`
Architect commands with command line overrides cannot target different builders. The
'${this.target}' target would run on projects ${projectNames.join()} which have the
following builders: ${'\n ' + builders.join('\n ')}
`);
}
if (!this.target) {
if (options.help) {
// This is a special case where we just return.
return;
}
}

const targetSpec: TargetSpecifier = this._makeTargetSpecifier(options);
const specifier = this._makeTargetSpecifier(options);
if (!specifier.project || !specifier.target) {
throw new Error('Cannot determine project or target for command.');
}

if (this.target && !targetSpec.project) {
const projects = this.getProjectNamesByTarget(this.target);
return;
}

if (projects.length === 1) {
// If there is a single target, use it to parse overrides.
targetSpec.project = projects[0];
const commandLeftovers = options['--'];
let projectName = options.project;
const targetProjectNames: string[] = [];
for (const name of this._workspace.listProjectNames()) {
if (this._architect.listProjectTargets(name).includes(this.target)) {
targetProjectNames.push(name);
}
}

if ((!targetSpec.project || !targetSpec.target) && !this.multiTarget) {
if (options.help) {
// This is a special case where we just return.
return;
}
if (targetProjectNames.length === 0) {
throw new Error(`No projects support the '${this.target}' target.`);
}

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

if (this.target) {
// Add options IF there's only one builder of this kind.
const targetSpec: TargetSpecifier = this._makeTargetSpecifier(options);
const projectNames = targetSpec.project
? [targetSpec.project]
: this.getProjectNamesByTarget(this.target);

const builderConfigurations: BuilderConfiguration[] = [];
for (const projectName of projectNames) {
const targetDesc = this._architect.getBuilderConfiguration({
project: projectName,
target: targetSpec.target,
if (!projectName && commandLeftovers && commandLeftovers.length > 0) {
const builderNames = new Set<string>();
const leftoverMap = new Map<string, { optionDefs: Option[], parsedOptions: Arguments }>();
let potentialProjectNames = new Set<string>(targetProjectNames);
for (const name of targetProjectNames) {
const builderConfig = this._architect.getBuilderConfiguration({
project: name,
target: this.target,
});

if (!builderConfigurations.find(b => b.builder === targetDesc.builder)) {
builderConfigurations.push(targetDesc);
if (this.multiTarget) {
builderNames.add(builderConfig.builder);
}

const builderDesc = await this._architect.getBuilderDescription(builderConfig).toPromise();
const optionDefs = await parseJsonSchemaToOptions(this._registry, builderDesc.schema);
const parsedOptions = parseArguments([...commandLeftovers], optionDefs);
const builderLeftovers = parsedOptions['--'] || [];
leftoverMap.set(name, { optionDefs, parsedOptions });

potentialProjectNames = new Set(builderLeftovers.filter(x => potentialProjectNames.has(x)));
}

if (builderConfigurations.length == 1) {
const builderConf = builderConfigurations[0];
const builderDesc = await this._architect.getBuilderDescription(builderConf).toPromise();
if (potentialProjectNames.size === 1) {
projectName = [...potentialProjectNames][0];

// remove the project name from the leftovers
const optionInfo = leftoverMap.get(projectName);
if (optionInfo) {
const locations = [];
let i = 0;
while (i < commandLeftovers.length) {
i = commandLeftovers.indexOf(projectName, i + 1);
if (i === -1) {
break;
}
locations.push(i);
}
delete optionInfo.parsedOptions['--'];
for (const location of locations) {
const tempLeftovers = [...commandLeftovers];
tempLeftovers.splice(location, 1);
const tempArgs = parseArguments([...tempLeftovers], optionInfo.optionDefs);
delete tempArgs['--'];
if (JSON.stringify(optionInfo.parsedOptions) === JSON.stringify(tempArgs)) {
options['--'] = tempLeftovers;
break;
}
}
}
}

if (!projectName && this.multiTarget && builderNames.size > 1) {
throw new Error(tags.oneLine`
Architect commands with command line overrides cannot target different builders. The
'${this.target}' target would run on projects ${targetProjectNames.join()} which have the
following builders: ${'\n ' + [...builderNames].join('\n ')}
`);
}
}

this.description.options.push(...(
await parseJsonSchemaToOptions(this._registry, builderDesc.schema)
));
if (!projectName && !this.multiTarget) {
const defaultProjectName = this._workspace.getDefaultProjectName();
if (targetProjectNames.length === 1) {
projectName = targetProjectNames[0];
} else if (defaultProjectName && targetProjectNames.includes(defaultProjectName)) {
projectName = defaultProjectName;
} else if (options.help) {
// This is a special case where we just return.
return;
} else {
throw new Error('Cannot determine project or target for command.');
}
}

options.project = projectName;

const builderConf = this._architect.getBuilderConfiguration({
project: projectName || (targetProjectNames.length > 0 ? targetProjectNames[0] : ''),
target: this.target,
});
const builderDesc = await this._architect.getBuilderDescription(builderConf).toPromise();

this.description.options.push(...(
await parseJsonSchemaToOptions(this._registry, builderDesc.schema)
));
}

async run(options: ArchitectCommandOptions & Arguments) {
Expand Down
5 changes: 5 additions & 0 deletions tests/legacy-cli/e2e/tests/basic/build.ts
Expand Up @@ -7,10 +7,15 @@ export default async function() {
await ng('build');
await expectFileToMatch('dist/test-project/index.html', 'main.js');

// Named Development build
await ng('build', 'test-project');
await ng('build', 'test-project', '--no-progress');
await ng('build', '--no-progress', 'test-project');

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

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

0 comments on commit 5e7f995

Please sign in to comment.