Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,11 @@ export abstract class ArchitectBaseCommandModule<T extends object>
return [];
}

return this.getOptionsForBuilder(builderConf);
}

protected async getOptionsForBuilder(builderConf: string): Promise<Option[]> {
const architectHost = this.getArchitectHost();
let builderDesc: NodeModulesBuilderInfo;
try {
builderDesc = await architectHost.resolveBuilder(builderConf);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ import {
Options,
OtherOptions,
} from './command-module';
import {
JsonHelpBuilder,
schemaOptionsToJsonHelpOptions,
setJsonHelpBuilders,
} from './utilities/json-help';

export interface ArchitectCommandArgs {
configuration?: string;
Expand All @@ -30,6 +35,16 @@ export abstract class ArchitectCommandModule
{
abstract readonly multiTarget: boolean;

/**
* A mapping of human-readable builder names to their corresponding Architect builder
* identifiers (for example, `{ Application: '@angular/build:application' }`).
*
* When defined, `--json-help` resolves the schema options for each builder and includes
* them under the `builders` property of the JSON help output instead of flattening
* a single project target's builder options into the command's top-level options.
*/
readonly builders?: Readonly<Record<string, string>>;

findDefaultBuilderName?(
project: workspaces.ProjectDefinition,
target: Target,
Expand Down Expand Up @@ -92,6 +107,19 @@ export abstract class ArchitectCommandModule
})
.strict();

if (jsonHelp && this.builders) {
const buildersHelp: JsonHelpBuilder[] = await Promise.all(
Object.entries(this.builders).map(async ([name, builderName]) => ({
name,
options: schemaOptionsToJsonHelpOptions(await this.getOptionsForBuilder(builderName)),
})),
);

setJsonHelpBuilders(this.context.yargsInstance, buildersHelp);

return localYargs;
}
Comment thread
alan-agius4 marked this conversation as resolved.
Resolved

if (!project) {
return localYargs;
}
Expand Down
67 changes: 63 additions & 4 deletions packages/angular/cli/src/command-builder/utilities/json-help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,31 @@
* found in the LICENSE file at https://angular.dev/license
*/

import { strings } from '@angular-devkit/core';
import { Argv } from 'yargs';
import { FullDescribe } from '../command-module';
import { Option } from './json-schema';

interface JsonHelpOption {
export interface JsonHelpOption {
name: string;
type?: string;
deprecated: boolean | string;
deprecated?: boolean | string;
aliases?: string[];
default?: string;
default?: string | number | boolean | unknown[];
required?: boolean;
positional?: number;
enum?: string[];
enum?: (string | number | boolean)[];
description?: string;
}

/**
* Represents a builder and its associated CLI options in JSON help output.
*/
export interface JsonHelpBuilder {
name: string;
options: JsonHelpOption[];
}

interface JsonHelpDescription {
shortDescription?: string;
longDescription?: string;
Expand All @@ -37,9 +47,56 @@ export interface JsonHelp extends JsonHelpDescription {
name: string;
command: string;
options: JsonHelpOption[];
builders?: JsonHelpBuilder[];
subcommands?: JsonHelpSubcommand[];
}

const jsonHelpBuilders = new WeakMap<Argv, JsonHelpBuilder[]>();

/**
* Associates a list of builder option groups with a yargs instance for `--json-help` serialization.
*/
export function setJsonHelpBuilders(localYargs: Argv, builders: JsonHelpBuilder[]): void {
jsonHelpBuilders.set(localYargs, builders);
}

/**
* Converts parsed JSON schema options into normalized and sorted `JsonHelpOption` entries.
*/
export function schemaOptionsToJsonHelpOptions(options: Option[]): JsonHelpOption[] {
const normalizeOptions: JsonHelpOption[] = [];

for (const option of options) {
if (option.hidden) {
continue;
}

let dashedName = strings.dasherize(option.name);
if (option.type === 'boolean' && dashedName.startsWith('no-')) {
dashedName = dashedName.slice(3);
}

const alias = Array.isArray(option.alias)
? option.alias
: option.alias
? [option.alias]
: undefined;

normalizeOptions.push({
name: dashedName,
type: option.type,
deprecated: option.deprecated,
aliases: alias && alias.length > 0 ? alias : undefined,
default: option.default as JsonHelpOption['default'],
enum: option.choices?.length ? (option.choices as JsonHelpOption['enum']) : undefined,
description: option.description,
positional: option.positional,
});
}

return normalizeOptions.sort((a, b) => a.name.localeCompare(b.name));
}

const yargsDefaultCommandRegExp = /^\$0|\*/;

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

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

Expand Down
170 changes: 170 additions & 0 deletions packages/angular/cli/src/command-builder/utilities/json-help_spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/**
* @license
* Copyright Google LLC 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.dev/license
*/

import yargs from 'yargs';
import { jsonHelpUsage, schemaOptionsToJsonHelpOptions, setJsonHelpBuilders } from './json-help';
import { Option } from './json-schema';

describe('json-help utilities', () => {
describe('schemaOptionsToJsonHelpOptions', () => {
it('normalizes and sorts schema options and filters out hidden options', () => {
const options: Option[] = [
{
name: 'watch',
type: 'boolean',
default: false,
description: 'Run build when files change.',
alias: [],
},
{
name: 'noProgress',
type: 'boolean',
description: 'Disable progress.',
alias: ['p'],
},
{
name: 'hiddenOption',
type: 'string',
hidden: true,
description: 'Should be filtered out.',
alias: [],
},
{
name: 'compilationMode',
type: 'string',
choices: ['full', 'partial'],
description: 'Compilation mode.',
alias: [],
},
{
name: 'noPrefixString',
type: 'string',
alias: 'n',
deprecated: 'Use another option.',
description: 'Non-boolean option starting with no-.',
},
];

expect(schemaOptionsToJsonHelpOptions(options)).toEqual([
{
name: 'compilation-mode',
type: 'string',
deprecated: undefined,
aliases: undefined,
default: undefined,
enum: ['full', 'partial'],
description: 'Compilation mode.',
positional: undefined,
},
{
name: 'no-prefix-string',
type: 'string',
deprecated: 'Use another option.',
aliases: ['n'],
default: undefined,
enum: undefined,
description: 'Non-boolean option starting with no-.',
positional: undefined,
},
{
name: 'progress',
type: 'boolean',
deprecated: undefined,
aliases: ['p'],
default: undefined,
enum: undefined,
description: 'Disable progress.',
positional: undefined,
},
{
name: 'watch',
type: 'boolean',
deprecated: undefined,
aliases: undefined,
default: false,
enum: undefined,
description: 'Run build when files change.',
positional: undefined,
},
]);
});
});

describe('setJsonHelpBuilders and jsonHelpUsage', () => {
it('includes builders in the jsonHelpUsage output when configured', () => {
const localYargs = yargs(['build', '--help'])
.scriptName('ng')
.usage('$0 build [project]', 'Build command')
.option('configuration', {
type: 'string',
alias: 'c',
describe: 'Builder configuration',
});

setJsonHelpBuilders(localYargs, [
{
name: 'Application',
options: [
{
name: 'aot',
type: 'boolean',
default: true,
description: 'Build using Ahead of Time compilation.',
},
],
},
{
name: 'Library',
options: [
{
name: 'compilation-mode',
type: 'string',
enum: ['full', 'partial'],
description: 'Angular compilation mode.',
},
],
},
]);

const parsed = JSON.parse(jsonHelpUsage(localYargs));
expect(parsed.builders).toEqual([
{
name: 'Application',
options: [
{
name: 'aot',
type: 'boolean',
default: true,
description: 'Build using Ahead of Time compilation.',
},
],
},
{
name: 'Library',
options: [
{
name: 'compilation-mode',
type: 'string',
enum: ['full', 'partial'],
description: 'Angular compilation mode.',
},
],
},
]);
});

it('omits builders when setJsonHelpBuilders is not called', () => {
const localYargs = yargs(['version', '--help'])
.scriptName('ng')
.usage('$0 version', 'Version command');

const parsed = JSON.parse(jsonHelpUsage(localYargs));
expect(parsed.builders).toBeUndefined();
});
});
});
4 changes: 4 additions & 0 deletions packages/angular/cli/src/commands/build/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export default class BuildCommandModule
implements CommandModuleImplementation
{
multiTarget = false;
override readonly builders = {
Application: '@angular/build:application',
Library: '@angular/build:library',
};
command = 'build [project]';
aliases = RootCommands['build'].aliases;
describe =
Expand Down
3 changes: 1 addition & 2 deletions packages/angular/cli/src/commands/build/long-description.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
The command can be used to build a project of type "application" or "library".
When used to build a library, a different builder is invoked, and only the `ts-config`, `configuration`, `poll` and `watch` options are applied.
All other options apply only to building applications.
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.

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.
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`.
Expand Down
4 changes: 4 additions & 0 deletions packages/angular/cli/src/commands/test/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export default class TestCommandModule
implements CommandModuleImplementation
{
multiTarget = true;
override readonly builders = {
'Unit-test': '@angular/build:unit-test',
'Karma': '@angular/build:karma',
};
command = 'test [project]';
aliases = RootCommands['test'].aliases;
describe = 'Runs unit tests in a project.';
Expand Down
Loading