mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-15 01:54:04 +08:00
274 lines
9.5 KiB
TypeScript
274 lines
9.5 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google Inc. All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.io/license
|
|
*/
|
|
import {
|
|
Architect,
|
|
BuildEvent,
|
|
BuilderConfiguration,
|
|
TargetSpecifier,
|
|
} from '@angular-devkit/architect';
|
|
import { experimental, json, schema, tags } from '@angular-devkit/core';
|
|
import { NodeJsSyncHost, createConsoleLogger } from '@angular-devkit/core/node';
|
|
import { from } from 'rxjs';
|
|
import { concatMap, map, tap, toArray } from 'rxjs/operators';
|
|
import { parseJsonSchemaToOptions } from '../utilities/json-schema';
|
|
import { BaseCommandOptions, Command } from './command';
|
|
import { Arguments } from './interface';
|
|
import { parseArguments } from './parser';
|
|
import { WorkspaceLoader } from './workspace-loader';
|
|
|
|
export interface ArchitectCommandOptions extends BaseCommandOptions {
|
|
project?: string;
|
|
configuration?: string;
|
|
prod?: boolean;
|
|
target?: string;
|
|
}
|
|
|
|
export abstract class ArchitectCommand<
|
|
T extends ArchitectCommandOptions = ArchitectCommandOptions,
|
|
> extends Command<ArchitectCommandOptions> {
|
|
private _host = new NodeJsSyncHost();
|
|
protected _architect: Architect;
|
|
protected _workspace: experimental.workspace.Workspace;
|
|
protected _logger = createConsoleLogger();
|
|
|
|
protected _registry: json.schema.SchemaRegistry;
|
|
|
|
// If this command supports running multiple targets.
|
|
protected multiTarget = false;
|
|
|
|
target: string | undefined;
|
|
|
|
public async initialize(options: ArchitectCommandOptions & Arguments): Promise<void> {
|
|
await super.initialize(options);
|
|
|
|
this._registry = new json.schema.CoreSchemaRegistry();
|
|
this._registry.addPostTransform(json.schema.transforms.addUndefinedDefaults);
|
|
|
|
await this._loadWorkspaceAndArchitect().toPromise();
|
|
|
|
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 ')}
|
|
`);
|
|
}
|
|
}
|
|
}
|
|
|
|
const targetSpec: TargetSpecifier = this._makeTargetSpecifier(options);
|
|
|
|
if (this.target && !targetSpec.project) {
|
|
const projects = this.getProjectNamesByTarget(this.target);
|
|
|
|
if (projects.length === 1) {
|
|
// If there is a single target, use it to parse overrides.
|
|
targetSpec.project = projects[0];
|
|
}
|
|
}
|
|
|
|
if ((!targetSpec.project || !targetSpec.target) && !this.multiTarget) {
|
|
if (options.help) {
|
|
// This is a special case where we just return.
|
|
return;
|
|
}
|
|
|
|
throw new Error('Cannot determine project or target for Architect command.');
|
|
}
|
|
|
|
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 (!builderConfigurations.find(b => b.builder === targetDesc.builder)) {
|
|
builderConfigurations.push(targetDesc);
|
|
}
|
|
}
|
|
|
|
if (builderConfigurations.length == 1) {
|
|
const builderConf = builderConfigurations[0];
|
|
const builderDesc = await this._architect.getBuilderDescription(builderConf).toPromise();
|
|
|
|
this.description.options.push(...(
|
|
await parseJsonSchemaToOptions(this._registry, builderDesc.schema)
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
async run(options: ArchitectCommandOptions & Arguments) {
|
|
return await this.runArchitectTarget(options);
|
|
}
|
|
|
|
protected async runSingleTarget(targetSpec: TargetSpecifier, options: string[]) {
|
|
// We need to build the builderSpec twice because architect does not understand
|
|
// overrides separately (getting the configuration builds the whole project, including
|
|
// overrides).
|
|
const builderConf = this._architect.getBuilderConfiguration(targetSpec);
|
|
const builderDesc = await this._architect.getBuilderDescription(builderConf).toPromise();
|
|
const targetOptionArray = await parseJsonSchemaToOptions(this._registry, builderDesc.schema);
|
|
const overrides = parseArguments(options, targetOptionArray);
|
|
|
|
if (overrides['--']) {
|
|
(overrides['--'] || []).forEach(additional => {
|
|
this.logger.warn(`Unknown option: '${additional.split(/=/)[0]}'`);
|
|
});
|
|
|
|
return 1;
|
|
}
|
|
const realBuilderConf = this._architect.getBuilderConfiguration({ ...targetSpec, overrides });
|
|
|
|
return this._architect.run(realBuilderConf, { logger: this._logger }).pipe(
|
|
map((buildEvent: BuildEvent) => buildEvent.success ? 0 : 1),
|
|
).toPromise();
|
|
}
|
|
|
|
protected async runArchitectTarget(
|
|
options: ArchitectCommandOptions & Arguments,
|
|
): Promise<number> {
|
|
const extra = options['--'] || [];
|
|
|
|
try {
|
|
const targetSpec = this._makeTargetSpecifier(options);
|
|
if (!targetSpec.project && this.target) {
|
|
// This runs each target sequentially.
|
|
// Running them in parallel would jumble the log messages.
|
|
return await from(this.getProjectNamesByTarget(this.target)).pipe(
|
|
concatMap(project => from(this.runSingleTarget({ ...targetSpec, project }, extra))),
|
|
toArray(),
|
|
map(results => results.every(res => res === 0) ? 0 : 1),
|
|
)
|
|
.toPromise();
|
|
} else {
|
|
return await this.runSingleTarget(targetSpec, extra);
|
|
}
|
|
} catch (e) {
|
|
if (e instanceof schema.SchemaValidationException) {
|
|
const newErrors: schema.SchemaValidatorError[] = [];
|
|
for (const schemaError of e.errors) {
|
|
if (schemaError.keyword === 'additionalProperties') {
|
|
const unknownProperty = schemaError.params.additionalProperty;
|
|
if (unknownProperty in options) {
|
|
const dashes = unknownProperty.length === 1 ? '-' : '--';
|
|
this.logger.fatal(`Unknown option: '${dashes}${unknownProperty}'`);
|
|
continue;
|
|
}
|
|
}
|
|
newErrors.push(schemaError);
|
|
}
|
|
|
|
if (newErrors.length > 0) {
|
|
this.logger.error(new schema.SchemaValidationException(newErrors).message);
|
|
}
|
|
|
|
return 1;
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
|
|
private getProjectNamesByTarget(targetName: string): string[] {
|
|
const allProjectsForTargetName = this._workspace.listProjectNames().map(projectName =>
|
|
this._architect.listProjectTargets(projectName).includes(targetName) ? projectName : null,
|
|
).filter(x => !!x) as string[];
|
|
|
|
if (this.multiTarget) {
|
|
// For multi target commands, we always list all projects that have the target.
|
|
return allProjectsForTargetName;
|
|
} else {
|
|
// For single target commands, we try the default project first,
|
|
// then the full list if it has a single project, then error out.
|
|
const maybeDefaultProject = this._workspace.getDefaultProjectName();
|
|
if (maybeDefaultProject && allProjectsForTargetName.includes(maybeDefaultProject)) {
|
|
return [maybeDefaultProject];
|
|
}
|
|
|
|
if (allProjectsForTargetName.length === 1) {
|
|
return allProjectsForTargetName;
|
|
}
|
|
|
|
throw new Error(`Could not determine a single project for the '${targetName}' target.`);
|
|
}
|
|
}
|
|
|
|
private _loadWorkspaceAndArchitect() {
|
|
const workspaceLoader = new WorkspaceLoader(this._host);
|
|
|
|
return workspaceLoader.loadWorkspace(this.workspace.root).pipe(
|
|
tap((workspace: experimental.workspace.Workspace) => this._workspace = workspace),
|
|
concatMap((workspace: experimental.workspace.Workspace) => {
|
|
return new Architect(workspace).loadArchitect();
|
|
}),
|
|
tap((architect: Architect) => this._architect = architect),
|
|
);
|
|
}
|
|
|
|
private _makeTargetSpecifier(commandOptions: ArchitectCommandOptions): TargetSpecifier {
|
|
let project, target, configuration;
|
|
|
|
if (commandOptions.target) {
|
|
[project, target, configuration] = commandOptions.target.split(':');
|
|
|
|
if (commandOptions.configuration) {
|
|
configuration = commandOptions.configuration;
|
|
}
|
|
} else {
|
|
project = commandOptions.project;
|
|
target = this.target;
|
|
configuration = commandOptions.configuration;
|
|
if (!configuration && commandOptions.prod) {
|
|
configuration = 'production';
|
|
}
|
|
}
|
|
|
|
if (!project) {
|
|
project = '';
|
|
}
|
|
if (!target) {
|
|
target = '';
|
|
}
|
|
|
|
return {
|
|
project,
|
|
configuration,
|
|
target,
|
|
};
|
|
}
|
|
}
|