Skip to content

Commit 5d50f59

Browse files
committed
fix(@angular/cli): include builder options for build and test in json-help
Allow ArchitectCommandModule subclasses to declare multiple builders so that --json-help resolves and outputs each builder's schema options separately under the builders field. Configure BuildCommandModule with the Application (@angular/build:application) and Library (@angular/build:library) builders, and TestCommandModule with the Unit-test (@angular/build:unit-test) and Karma (@angular/build:karma) builders.
1 parent a84f9fa commit 5d50f59

7 files changed

Lines changed: 275 additions & 6 deletions

File tree

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,11 @@ export abstract class ArchitectBaseCommandModule<T extends object>
183183
return [];
184184
}
185185

186+
return this.getOptionsForBuilder(builderConf);
187+
}
188+
189+
protected async getOptionsForBuilder(builderConf: string): Promise<Option[]> {
190+
const architectHost = this.getArchitectHost();
186191
let builderDesc: NodeModulesBuilderInfo;
187192
try {
188193
builderDesc = await architectHost.resolveBuilder(builderConf);

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ import {
1818
Options,
1919
OtherOptions,
2020
} from './command-module';
21+
import {
22+
JsonHelpBuilder,
23+
schemaOptionsToJsonHelpOptions,
24+
setJsonHelpBuilders,
25+
} from './utilities/json-help';
2126

2227
export interface ArchitectCommandArgs {
2328
configuration?: string;
@@ -30,6 +35,16 @@ export abstract class ArchitectCommandModule
3035
{
3136
abstract readonly multiTarget: boolean;
3237

38+
/**
39+
* A mapping of human-readable builder names to their corresponding Architect builder
40+
* identifiers (for example, `{ Application: '@angular/build:application' }`).
41+
*
42+
* When defined, `--json-help` resolves the schema options for each builder and includes
43+
* them under the `builders` property of the JSON help output instead of flattening
44+
* a single project target's builder options into the command's top-level options.
45+
*/
46+
readonly builders?: Readonly<Record<string, string>>;
47+
3348
findDefaultBuilderName?(
3449
project: workspaces.ProjectDefinition,
3550
target: Target,
@@ -92,6 +107,19 @@ export abstract class ArchitectCommandModule
92107
})
93108
.strict();
94109

110+
if (jsonHelp && this.builders) {
111+
const buildersHelp: JsonHelpBuilder[] = await Promise.all(
112+
Object.entries(this.builders).map(async ([name, builderName]) => ({
113+
name,
114+
options: schemaOptionsToJsonHelpOptions(await this.getOptionsForBuilder(builderName)),
115+
})),
116+
);
117+
118+
setJsonHelpBuilders(this.context.yargsInstance, buildersHelp);
119+
120+
return localYargs;
121+
}
122+
95123
if (!project) {
96124
return localYargs;
97125
}

‎packages/angular/cli/src/command-builder/utilities/json-help.ts‎

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,31 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9+
import { strings } from '@angular-devkit/core';
910
import { Argv } from 'yargs';
1011
import { FullDescribe } from '../command-module';
12+
import { Option } from './json-schema';
1113

12-
interface JsonHelpOption {
14+
export interface JsonHelpOption {
1315
name: string;
1416
type?: string;
15-
deprecated: boolean | string;
17+
deprecated?: boolean | string;
1618
aliases?: string[];
17-
default?: string;
19+
default?: string | number | boolean | unknown[];
1820
required?: boolean;
1921
positional?: number;
20-
enum?: string[];
22+
enum?: (string | number | boolean)[];
2123
description?: string;
2224
}
2325

26+
/**
27+
* Represents a builder and its associated CLI options in JSON help output.
28+
*/
29+
export interface JsonHelpBuilder {
30+
name: string;
31+
options: JsonHelpOption[];
32+
}
33+
2434
interface JsonHelpDescription {
2535
shortDescription?: string;
2636
longDescription?: string;
@@ -37,9 +47,56 @@ export interface JsonHelp extends JsonHelpDescription {
3747
name: string;
3848
command: string;
3949
options: JsonHelpOption[];
50+
builders?: JsonHelpBuilder[];
4051
subcommands?: JsonHelpSubcommand[];
4152
}
4253

54+
const jsonHelpBuilders = new WeakMap<Argv, JsonHelpBuilder[]>();
55+
56+
/**
57+
* Associates a list of builder option groups with a yargs instance for `--json-help` serialization.
58+
*/
59+
export function setJsonHelpBuilders(localYargs: Argv, builders: JsonHelpBuilder[]): void {
60+
jsonHelpBuilders.set(localYargs, builders);
61+
}
62+
63+
/**
64+
* Converts parsed JSON schema options into normalized and sorted `JsonHelpOption` entries.
65+
*/
66+
export function schemaOptionsToJsonHelpOptions(options: Option[]): JsonHelpOption[] {
67+
const normalizeOptions: JsonHelpOption[] = [];
68+
69+
for (const option of options) {
70+
if (option.hidden) {
71+
continue;
72+
}
73+
74+
let dashedName = strings.dasherize(option.name);
75+
if (option.type === 'boolean' && dashedName.startsWith('no-')) {
76+
dashedName = dashedName.slice(3);
77+
}
78+
79+
const alias = Array.isArray(option.alias)
80+
? option.alias
81+
: option.alias
82+
? [option.alias]
83+
: undefined;
84+
85+
normalizeOptions.push({
86+
name: dashedName,
87+
type: option.type,
88+
deprecated: option.deprecated,
89+
aliases: alias && alias.length > 0 ? alias : undefined,
90+
default: option.default as JsonHelpOption['default'],
91+
enum: option.choices?.length ? (option.choices as JsonHelpOption['enum']) : undefined,
92+
description: option.description,
93+
positional: option.positional,
94+
});
95+
}
96+
97+
return normalizeOptions.sort((a, b) => a.name.localeCompare(b.name));
98+
}
99+
43100
const yargsDefaultCommandRegExp = /^\$0|\*/;
44101

45102
export function jsonHelpUsage(localYargs: Argv): string {
@@ -123,12 +180,14 @@ export function jsonHelpUsage(localYargs: Argv): string {
123180
const [command, rawDescription] = usageInstance.getUsage()[0] ?? [];
124181
const defaultSubCommand = subcommands.find((x) => x.default)?.command ?? '';
125182
const otherSubcommands = subcommands.filter((s) => !s.default);
183+
const builders = jsonHelpBuilders.get(localYargs);
126184

127185
const output: JsonHelp = {
128186
name: [...context.commands].pop(),
129187
command: `${command?.replace(yargsDefaultCommandRegExp, localYargsInstance['$0'])}${defaultSubCommand}`,
130188
...parseDescription(rawDescription),
131189
options: normalizeOptions.sort((a, b) => a.name.localeCompare(b.name)),
190+
builders: builders?.length ? builders : undefined,
132191
subcommands: otherSubcommands.length ? otherSubcommands : undefined,
133192
};
134193

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
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.dev/license
7+
*/
8+
9+
import yargs from 'yargs';
10+
import { jsonHelpUsage, schemaOptionsToJsonHelpOptions, setJsonHelpBuilders } from './json-help';
11+
import { Option } from './json-schema';
12+
13+
describe('json-help utilities', () => {
14+
describe('schemaOptionsToJsonHelpOptions', () => {
15+
it('normalizes and sorts schema options and filters out hidden options', () => {
16+
const options: Option[] = [
17+
{
18+
name: 'watch',
19+
type: 'boolean',
20+
default: false,
21+
description: 'Run build when files change.',
22+
alias: [],
23+
},
24+
{
25+
name: 'noProgress',
26+
type: 'boolean',
27+
description: 'Disable progress.',
28+
alias: ['p'],
29+
},
30+
{
31+
name: 'hiddenOption',
32+
type: 'string',
33+
hidden: true,
34+
description: 'Should be filtered out.',
35+
alias: [],
36+
},
37+
{
38+
name: 'compilationMode',
39+
type: 'string',
40+
choices: ['full', 'partial'],
41+
description: 'Compilation mode.',
42+
alias: [],
43+
},
44+
{
45+
name: 'noPrefixString',
46+
type: 'string',
47+
alias: 'n',
48+
deprecated: 'Use another option.',
49+
description: 'Non-boolean option starting with no-.',
50+
},
51+
];
52+
53+
expect(schemaOptionsToJsonHelpOptions(options)).toEqual([
54+
{
55+
name: 'compilation-mode',
56+
type: 'string',
57+
deprecated: undefined,
58+
aliases: undefined,
59+
default: undefined,
60+
enum: ['full', 'partial'],
61+
description: 'Compilation mode.',
62+
positional: undefined,
63+
},
64+
{
65+
name: 'no-prefix-string',
66+
type: 'string',
67+
deprecated: 'Use another option.',
68+
aliases: ['n'],
69+
default: undefined,
70+
enum: undefined,
71+
description: 'Non-boolean option starting with no-.',
72+
positional: undefined,
73+
},
74+
{
75+
name: 'progress',
76+
type: 'boolean',
77+
deprecated: undefined,
78+
aliases: ['p'],
79+
default: undefined,
80+
enum: undefined,
81+
description: 'Disable progress.',
82+
positional: undefined,
83+
},
84+
{
85+
name: 'watch',
86+
type: 'boolean',
87+
deprecated: undefined,
88+
aliases: undefined,
89+
default: false,
90+
enum: undefined,
91+
description: 'Run build when files change.',
92+
positional: undefined,
93+
},
94+
]);
95+
});
96+
});
97+
98+
describe('setJsonHelpBuilders and jsonHelpUsage', () => {
99+
it('includes builders in the jsonHelpUsage output when configured', () => {
100+
const localYargs = yargs(['build', '--help'])
101+
.scriptName('ng')
102+
.usage('$0 build [project]', 'Build command')
103+
.option('configuration', {
104+
type: 'string',
105+
alias: 'c',
106+
describe: 'Builder configuration',
107+
});
108+
109+
setJsonHelpBuilders(localYargs, [
110+
{
111+
name: 'Application',
112+
options: [
113+
{
114+
name: 'aot',
115+
type: 'boolean',
116+
default: true,
117+
description: 'Build using Ahead of Time compilation.',
118+
},
119+
],
120+
},
121+
{
122+
name: 'Library',
123+
options: [
124+
{
125+
name: 'compilation-mode',
126+
type: 'string',
127+
enum: ['full', 'partial'],
128+
description: 'Angular compilation mode.',
129+
},
130+
],
131+
},
132+
]);
133+
134+
const parsed = JSON.parse(jsonHelpUsage(localYargs));
135+
expect(parsed.builders).toEqual([
136+
{
137+
name: 'Application',
138+
options: [
139+
{
140+
name: 'aot',
141+
type: 'boolean',
142+
default: true,
143+
description: 'Build using Ahead of Time compilation.',
144+
},
145+
],
146+
},
147+
{
148+
name: 'Library',
149+
options: [
150+
{
151+
name: 'compilation-mode',
152+
type: 'string',
153+
enum: ['full', 'partial'],
154+
description: 'Angular compilation mode.',
155+
},
156+
],
157+
},
158+
]);
159+
});
160+
161+
it('omits builders when setJsonHelpBuilders is not called', () => {
162+
const localYargs = yargs(['version', '--help'])
163+
.scriptName('ng')
164+
.usage('$0 version', 'Version command');
165+
166+
const parsed = JSON.parse(jsonHelpUsage(localYargs));
167+
expect(parsed.builders).toBeUndefined();
168+
});
169+
});
170+
});

‎packages/angular/cli/src/commands/build/cli.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ export default class BuildCommandModule
1616
implements CommandModuleImplementation
1717
{
1818
multiTarget = false;
19+
override readonly builders = {
20+
Application: '@angular/build:application',
21+
Library: '@angular/build:library',
22+
};
1923
command = 'build [project]';
2024
aliases = RootCommands['build'].aliases;
2125
describe =

‎packages/angular/cli/src/commands/build/long-description.md‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
The command can be used to build a project of type "application" or "library".
2-
When used to build a library, a different builder is invoked, and only the `ts-config`, `configuration`, `poll` and `watch` options are applied.
3-
All other options apply only to building applications.
2+
Depending on the project type, the CLI invokes either the application builder (`@angular/build:application`) or the library builder (`@angular/build:library`), each supporting its own set of options.
43

54
The application builder uses the [esbuild](https://esbuild.github.io/) build tool, with default configuration options specified in the workspace configuration file (`angular.json`) or with a named alternative configuration.
65
A "development" configuration is created by default when you use the CLI to create the project, and you can use that configuration by specifying the `--configuration development`.

‎packages/angular/cli/src/commands/test/cli.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ export default class TestCommandModule
1616
implements CommandModuleImplementation
1717
{
1818
multiTarget = true;
19+
override readonly builders = {
20+
'Unit-test': '@angular/build:unit-test',
21+
'Karma': '@angular/build:karma',
22+
};
1923
command = 'test [project]';
2024
aliases = RootCommands['test'].aliases;
2125
describe = 'Runs unit tests in a project.';

0 commit comments

Comments
 (0)