Skip to content

Commit 98984bf

Browse files
committed
refactor(@angular/cli): move move architect common logic into a base class
1 parent ead9aa1 commit 98984bf

File tree

6 files changed

+198
-205
lines changed

6 files changed

+198
-205
lines changed
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import { Architect, Target } from '@angular-devkit/architect';
10+
import { WorkspaceNodeModulesArchitectHost } from '@angular-devkit/architect/node';
11+
import { json } from '@angular-devkit/core';
12+
import { existsSync } from 'fs';
13+
import { resolve } from 'path';
14+
import { isPackageNameSafeForAnalytics } from '../analytics/analytics';
15+
import { getPackageManager } from '../utilities/package-manager';
16+
import {
17+
CommandModule,
18+
CommandModuleError,
19+
CommandModuleImplementation,
20+
CommandScope,
21+
OtherOptions,
22+
} from './command-module';
23+
import { Option, parseJsonSchemaToOptions } from './utilities/json-schema';
24+
25+
export abstract class ArchitectBaseCommandModule<T>
26+
extends CommandModule<T>
27+
implements CommandModuleImplementation<T>
28+
{
29+
static override scope = CommandScope.In;
30+
protected override shouldReportAnalytics = false;
31+
protected readonly missingErrorTarget: string | undefined;
32+
33+
protected async runSingleTarget(target: Target, options: OtherOptions): Promise<number> {
34+
// Remove options
35+
const architectHost = await this.getArchitectHost();
36+
37+
let builderName: string;
38+
try {
39+
builderName = await architectHost.getBuilderNameForTarget(target);
40+
} catch (e) {
41+
throw new CommandModuleError(this.missingErrorTarget ?? e.message);
42+
}
43+
44+
await this.reportAnalytics({
45+
...(await architectHost.getOptionsForTarget(target)),
46+
...options,
47+
});
48+
49+
const { logger } = this.context;
50+
51+
const run = await this.getArchitect().scheduleTarget(target, options as json.JsonObject, {
52+
logger,
53+
analytics: isPackageNameSafeForAnalytics(builderName) ? await this.getAnalytics() : undefined,
54+
});
55+
56+
const { error, success } = await run.output.toPromise();
57+
await run.stop();
58+
59+
if (error) {
60+
logger.error(error);
61+
}
62+
63+
return success ? 0 : 1;
64+
}
65+
66+
private _architectHost: WorkspaceNodeModulesArchitectHost | undefined;
67+
protected getArchitectHost(): WorkspaceNodeModulesArchitectHost {
68+
if (this._architectHost) {
69+
return this._architectHost;
70+
}
71+
72+
const { workspace } = this.context;
73+
if (!workspace) {
74+
throw new CommandModuleError('A workspace is required for this command.');
75+
}
76+
77+
return (this._architectHost = new WorkspaceNodeModulesArchitectHost(
78+
workspace,
79+
workspace.basePath,
80+
));
81+
}
82+
83+
private _architect: Architect | undefined;
84+
protected getArchitect(): Architect {
85+
if (this._architect) {
86+
return this._architect;
87+
}
88+
89+
const registry = new json.schema.CoreSchemaRegistry();
90+
registry.addPostTransform(json.schema.transforms.addUndefinedDefaults);
91+
registry.useXDeprecatedProvider((msg) => this.context.logger.warn(msg));
92+
93+
const { workspace } = this.context;
94+
if (!workspace) {
95+
throw new CommandModuleError('Cannot invoke this command outside of a workspace');
96+
}
97+
98+
const architectHost = this.getArchitectHost();
99+
100+
return (this._architect = new Architect(architectHost, registry));
101+
}
102+
103+
protected async getArchitectTargetOptions(target: Target): Promise<Option[]> {
104+
const { workspace } = this.context;
105+
if (!workspace) {
106+
throw new CommandModuleError('A workspace is required for this command.');
107+
}
108+
109+
const architectHost = new WorkspaceNodeModulesArchitectHost(workspace, workspace.basePath);
110+
const builderConf = await architectHost.getBuilderNameForTarget(target);
111+
112+
let builderDesc;
113+
try {
114+
builderDesc = await architectHost.resolveBuilder(builderConf);
115+
} catch (e) {
116+
if (e.code === 'MODULE_NOT_FOUND') {
117+
await this.warnOnMissingNodeModules();
118+
throw new CommandModuleError(`Could not find the '${builderConf}' builder's node package.`);
119+
}
120+
121+
throw e;
122+
}
123+
124+
return parseJsonSchemaToOptions(
125+
new json.schema.CoreSchemaRegistry(),
126+
builderDesc.optionSchema as json.JsonObject,
127+
true,
128+
);
129+
}
130+
131+
private async warnOnMissingNodeModules(): Promise<void> {
132+
const basePath = this.context.workspace?.basePath;
133+
if (!basePath) {
134+
return;
135+
}
136+
137+
// Check for a `node_modules` directory (npm, yarn non-PnP, etc.)
138+
if (existsSync(resolve(basePath, 'node_modules'))) {
139+
return;
140+
}
141+
142+
// Check for yarn PnP files
143+
if (
144+
existsSync(resolve(basePath, '.pnp.js')) ||
145+
existsSync(resolve(basePath, '.pnp.cjs')) ||
146+
existsSync(resolve(basePath, '.pnp.mjs'))
147+
) {
148+
return;
149+
}
150+
151+
const packageManager = await getPackageManager(basePath);
152+
this.context.logger.warn(
153+
`Node packages may not be installed. Try installing with '${packageManager} install'.`,
154+
);
155+
}
156+
}

packages/angular/cli/src/command-builder/architect-command-module.ts

Lines changed: 15 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -6,34 +6,25 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import { Architect, Target } from '@angular-devkit/architect';
10-
import { WorkspaceNodeModulesArchitectHost } from '@angular-devkit/architect/node';
11-
import { json } from '@angular-devkit/core';
129
import { Argv } from 'yargs';
13-
import { isPackageNameSafeForAnalytics } from '../analytics/analytics';
10+
import { ArchitectBaseCommandModule } from './architect-base-command-module';
1411
import {
15-
CommandModule,
1612
CommandModuleError,
1713
CommandModuleImplementation,
18-
CommandScope,
1914
Options,
2015
OtherOptions,
2116
} from './command-module';
22-
import { getArchitectTargetOptions } from './utilities/architect';
2317

2418
export interface ArchitectCommandArgs {
2519
configuration?: string;
2620
project?: string;
2721
}
2822

2923
export abstract class ArchitectCommandModule
30-
extends CommandModule<ArchitectCommandArgs>
24+
extends ArchitectBaseCommandModule<ArchitectCommandArgs>
3125
implements CommandModuleImplementation<ArchitectCommandArgs>
3226
{
33-
static override scope = CommandScope.In;
3427
abstract readonly multiTarget: boolean;
35-
readonly missingErrorTarget: string | undefined;
36-
protected override shouldReportAnalytics = false;
3728

3829
async builder(argv: Argv): Promise<Argv<ArchitectCommandArgs>> {
3930
const localYargs: Argv<ArchitectCommandArgs> = argv
@@ -52,35 +43,32 @@ export abstract class ArchitectCommandModule
5243
})
5344
.strict();
5445

55-
const targetSpecifier = this.makeTargetSpecifier();
56-
if (!targetSpecifier.project) {
46+
const project = this.getArchitectProject();
47+
if (!project) {
5748
return localYargs;
5849
}
5950

60-
const schemaOptions = await getArchitectTargetOptions(this.context, targetSpecifier);
51+
const target = this.getArchitectTarget();
52+
const schemaOptions = await this.getArchitectTargetOptions({
53+
project,
54+
target,
55+
});
6156

6257
return this.addSchemaOptionsToCommand(localYargs, schemaOptions);
6358
}
6459

65-
async run(options: Options<ArchitectCommandArgs>): Promise<number | void> {
60+
async run(options: Options<ArchitectCommandArgs> & OtherOptions): Promise<number | void> {
6661
const { logger, workspace } = this.context;
6762
if (!workspace) {
6863
logger.fatal('A workspace is required for this command.');
6964

7065
return 1;
7166
}
7267

73-
const registry = new json.schema.CoreSchemaRegistry();
74-
registry.addPostTransform(json.schema.transforms.addUndefinedDefaults);
75-
registry.useXDeprecatedProvider((msg) => this.context.logger.warn(msg));
76-
77-
const architectHost = new WorkspaceNodeModulesArchitectHost(workspace, workspace.basePath);
78-
const architect = new Architect(architectHost, registry);
79-
80-
const targetSpec = this.makeTargetSpecifier(options);
81-
if (!targetSpec.project) {
82-
const target = this.getArchitectTarget();
68+
const target = this.getArchitectTarget();
69+
const { configuration = '', project, ...architectOptions } = options;
8370

71+
if (!project) {
8472
// This runs each target sequentially.
8573
// Running them in parallel would jumble the log messages.
8674
let result = 0;
@@ -92,12 +80,12 @@ export abstract class ArchitectCommandModule
9280
}
9381

9482
for (const project of projectNames) {
95-
result |= await this.runSingleTarget({ ...targetSpec, project }, options, architect);
83+
result |= await this.runSingleTarget({ configuration, target, project }, architectOptions);
9684
}
9785

9886
return result;
9987
} else {
100-
return await this.runSingleTarget(targetSpec, options, architect);
88+
return await this.runSingleTarget({ configuration, target, project }, architectOptions);
10189
}
10290
}
10391

@@ -128,14 +116,6 @@ export abstract class ArchitectCommandModule
128116
return this.command?.split(' ', 1)[0];
129117
}
130118

131-
private makeTargetSpecifier(options?: Options<ArchitectCommandArgs>): Target {
132-
return {
133-
project: options?.project ?? this.getArchitectProject() ?? '',
134-
target: this.getArchitectTarget(),
135-
configuration: options?.configuration ?? '',
136-
};
137-
}
138-
139119
private getProjectNamesByTarget(target: string): string[] | undefined {
140120
const workspace = this.context.workspace;
141121
if (!workspace) {
@@ -174,59 +154,4 @@ export abstract class ArchitectCommandModule
174154

175155
return undefined;
176156
}
177-
178-
private async runSingleTarget(
179-
target: Target,
180-
options: Options<ArchitectCommandArgs> & OtherOptions,
181-
architect: Architect,
182-
): Promise<number> {
183-
// Remove options
184-
const { configuration, project, ...extraOptions } = options;
185-
const architectHost = await this.getArchitectHost();
186-
187-
let builderName: string;
188-
try {
189-
builderName = await architectHost.getBuilderNameForTarget(target);
190-
} catch (e) {
191-
throw new CommandModuleError(this.missingErrorTarget ?? e.message);
192-
}
193-
194-
await this.reportAnalytics({
195-
...(await architectHost.getOptionsForTarget(target)),
196-
...extraOptions,
197-
});
198-
199-
const { logger } = this.context;
200-
201-
const run = await architect.scheduleTarget(target, extraOptions as json.JsonObject, {
202-
logger,
203-
analytics: isPackageNameSafeForAnalytics(builderName) ? await this.getAnalytics() : undefined,
204-
});
205-
206-
const { error, success } = await run.output.toPromise();
207-
await run.stop();
208-
209-
if (error) {
210-
logger.error(error);
211-
}
212-
213-
return success ? 0 : 1;
214-
}
215-
216-
private _architectHost: WorkspaceNodeModulesArchitectHost | undefined;
217-
private getArchitectHost(): WorkspaceNodeModulesArchitectHost {
218-
if (this._architectHost) {
219-
return this._architectHost;
220-
}
221-
222-
const { workspace } = this.context;
223-
if (!workspace) {
224-
throw new CommandModuleError('A workspace is required for this command.');
225-
}
226-
227-
return (this._architectHost = new WorkspaceNodeModulesArchitectHost(
228-
workspace,
229-
workspace.basePath,
230-
));
231-
}
232157
}

packages/angular/cli/src/command-builder/command-module.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,22 @@ import { analytics, logging, normalize, strings } from '@angular-devkit/core';
1010
import { readFileSync } from 'fs';
1111
import * as path from 'path';
1212
import {
13+
ArgumentsCamelCase,
1314
Argv,
1415
CamelCaseKey,
1516
PositionalOptions,
1617
CommandModule as YargsCommandModule,
1718
Options as YargsOptions,
1819
} from 'yargs';
20+
import { Parser } from 'yargs/helpers';
1921
import { createAnalytics } from '../analytics/analytics';
2022
import { AngularWorkspace } from '../utilities/config';
2123
import { Option } from './utilities/json-schema';
2224

25+
const yargsParser = Parser as unknown as typeof Parser.default & {
26+
camelCase(str: string): string;
27+
};
28+
2329
export type Options<T> = { [key in keyof T as CamelCaseKey<key>]: T[key] };
2430

2531
export enum CommandScope {
@@ -55,8 +61,6 @@ export interface CommandModuleImplementation<T extends {} = {}>
5561
builder(argv: Argv): Promise<Argv<T>> | Argv<T>;
5662
/** A function which will be passed the parsed argv. */
5763
run(options: Options<T> & OtherOptions): Promise<number | void> | number | void;
58-
/** a function which will be passed the parsed argv. */
59-
handler(args: Options<T> & OtherOptions): Promise<void> | void;
6064
}
6165

6266
export interface FullDescribe {
@@ -106,16 +110,24 @@ export abstract class CommandModule<T extends {} = {}> implements CommandModuleI
106110
abstract builder(argv: Argv): Promise<Argv<T>> | Argv<T>;
107111
abstract run(options: Options<T> & OtherOptions): Promise<number | void> | number | void;
108112

109-
async handler(args: Options<T> & OtherOptions): Promise<void> {
113+
async handler(args: ArgumentsCamelCase<T> & OtherOptions): Promise<void> {
114+
const { _, $0, ...options } = args;
115+
116+
// Camelize options as yargs will return the object in kebab-case when camel casing is disabled.
117+
const camelCasedOptions: Record<string, unknown> = {};
118+
for (const [key, value] of Object.entries(options)) {
119+
camelCasedOptions[yargsParser.camelCase(key)] = value;
120+
}
121+
110122
// Gather and report analytics.
111123
const analytics = await this.getAnalytics();
112124
if (this.shouldReportAnalytics) {
113-
await this.reportAnalytics(args);
125+
await this.reportAnalytics(camelCasedOptions);
114126
}
115127

116128
// Run and time command.
117129
const startTime = Date.now();
118-
const result = await this.run(args);
130+
const result = await this.run(camelCasedOptions as Options<T> & OtherOptions);
119131
const endTime = Date.now();
120132

121133
analytics.timing(this.commandName, 'duration', endTime - startTime);
@@ -127,7 +139,7 @@ export abstract class CommandModule<T extends {} = {}> implements CommandModuleI
127139
}
128140

129141
async reportAnalytics(
130-
options: Options<T> & OtherOptions,
142+
options: (Options<T> & OtherOptions) | OtherOptions,
131143
paths: string[] = [],
132144
dimensions: (boolean | number | string)[] = [],
133145
): Promise<void> {

0 commit comments

Comments
 (0)