Skip to content

Commit 607a723

Browse files
alan-agius4dgp1130
authored andcommitted
feat(@angular/cli): add support for auto completion
To enable bash and zsh real-time type-ahead autocompletion, copy and paste the generated script by the `ng completion` command to your `.bashrc`, `.bash_profile`, `.zshrc` or `.zsh_profile`. Closes #11043
1 parent 95954bb commit 607a723

File tree

9 files changed

+174
-15
lines changed

9 files changed

+174
-15
lines changed

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

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
*/
88

99
import { Architect, Target } from '@angular-devkit/architect';
10-
import { WorkspaceNodeModulesArchitectHost } from '@angular-devkit/architect/node';
10+
import {
11+
NodeModulesBuilderInfo,
12+
WorkspaceNodeModulesArchitectHost,
13+
} from '@angular-devkit/architect/node';
1114
import { json } from '@angular-devkit/core';
1215
import { spawnSync } from 'child_process';
1316
import { existsSync } from 'fs';
@@ -100,9 +103,15 @@ export abstract class ArchitectBaseCommandModule<T>
100103

101104
protected async getArchitectTargetOptions(target: Target): Promise<Option[]> {
102105
const architectHost = this.getArchitectHost();
103-
const builderConf = await architectHost.getBuilderNameForTarget(target);
106+
let builderConf: string;
107+
108+
try {
109+
builderConf = await architectHost.getBuilderNameForTarget(target);
110+
} catch {
111+
return [];
112+
}
104113

105-
let builderDesc;
114+
let builderDesc: NodeModulesBuilderInfo;
106115
try {
107116
builderDesc = await architectHost.resolveBuilder(builderConf);
108117
} catch (e) {

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

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,15 @@ export abstract class ArchitectCommandModule
2929
abstract readonly multiTarget: boolean;
3030

3131
async builder(argv: Argv): Promise<Argv<ArchitectCommandArgs>> {
32+
const project = this.getArchitectProject();
33+
const { jsonHelp, getYargsCompletions, help } = this.context.args.options;
34+
3235
const localYargs: Argv<ArchitectCommandArgs> = argv
3336
.positional('project', {
3437
describe: 'The name of the project to build. Can be an application or a library.',
3538
type: 'string',
39+
// Hide choices from JSON help so that we don't display them in AIO.
40+
choices: jsonHelp ? undefined : this.getProjectChoices(),
3641
})
3742
.option('configuration', {
3843
describe:
@@ -42,10 +47,15 @@ export abstract class ArchitectCommandModule
4247
`For more information, see https://angular.io/guide/workspace-config#alternate-build-configurations.`,
4348
alias: 'c',
4449
type: 'string',
50+
// Show only in when using --help and auto completion because otherwise comma seperated configuration values will be invalid.
51+
// Also, hide choices from JSON help so that we don't display them in AIO.
52+
choices:
53+
(getYargsCompletions || help) && !jsonHelp && project
54+
? this.getConfigurationChoices(project)
55+
: undefined,
4556
})
4657
.strict();
4758

48-
const project = this.getArchitectProject();
4959
if (!project) {
5060
return localYargs;
5161
}
@@ -92,11 +102,7 @@ export abstract class ArchitectCommandModule
92102
const [, projectName] = this.context.args.positional;
93103

94104
if (projectName) {
95-
if (!workspace.projects.has(projectName)) {
96-
throw new CommandModuleError(`Project '${projectName}' does not exist.`);
97-
}
98-
99-
return projectName;
105+
return workspace.projects.has(projectName) ? projectName : undefined;
100106
}
101107

102108
const target = this.getArchitectTarget();
@@ -136,4 +142,24 @@ export abstract class ArchitectCommandModule
136142

137143
return undefined;
138144
}
145+
146+
/** @returns a sorted list of project names to be used for auto completion. */
147+
private getProjectChoices(): string[] | undefined {
148+
const { workspace } = this.context;
149+
150+
return workspace ? [...workspace.projects.keys()].sort() : undefined;
151+
}
152+
153+
/** @returns a sorted list of configuration names to be used for auto completion. */
154+
private getConfigurationChoices(project: string): string[] | undefined {
155+
const projectDefinition = this.context.workspace?.projects.get(project);
156+
if (!projectDefinition) {
157+
return undefined;
158+
}
159+
160+
const target = this.getArchitectTarget();
161+
const configurations = projectDefinition.targets.get(target)?.configurations;
162+
163+
return configurations ? Object.keys(configurations).sort() : undefined;
164+
}
139165
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export interface CommandContext {
4848
options: {
4949
help: boolean;
5050
jsonHelp: boolean;
51+
getYargsCompletions: boolean;
5152
} & Record<string, unknown>;
5253
};
5354
}

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

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { AddCommandModule } from '../commands/add/cli';
1313
import { AnalyticsCommandModule } from '../commands/analytics/cli';
1414
import { BuildCommandModule } from '../commands/build/cli';
1515
import { CacheCommandModule } from '../commands/cache/cli';
16+
import { CompletionCommandModule } from '../commands/completion/cli';
1617
import { ConfigCommandModule } from '../commands/config/cli';
1718
import { DeployCommandModule } from '../commands/deploy/cli';
1819
import { DocCommandModule } from '../commands/doc/cli';
@@ -54,18 +55,26 @@ const COMMANDS = [
5455
UpdateCommandModule,
5556
RunCommandModule,
5657
CacheCommandModule,
58+
CompletionCommandModule,
5759
].sort(); // Will be sorted by class name.
5860

5961
const yargsParser = Parser as unknown as typeof Parser.default;
6062

6163
export async function runCommand(args: string[], logger: logging.Logger): Promise<number> {
6264
const {
6365
$0,
64-
_: positional,
66+
_,
6567
help = false,
6668
jsonHelp = false,
69+
getYargsCompletions = false,
6770
...rest
68-
} = yargsParser(args, { boolean: ['help', 'json-help'], alias: { 'collection': 'c' } });
71+
} = yargsParser(args, {
72+
boolean: ['help', 'json-help', 'get-yargs-completions'],
73+
alias: { 'collection': 'c' },
74+
});
75+
76+
// When `getYargsCompletions` is true the scriptName 'ng' at index 0 is not removed.
77+
const positional = getYargsCompletions ? _.slice(1) : _;
6978

7079
let workspace: AngularWorkspace | undefined;
7180
let globalConfiguration: AngularWorkspace | undefined;
@@ -93,6 +102,7 @@ export async function runCommand(args: string[], logger: logging.Logger): Promis
93102
options: {
94103
help,
95104
jsonHelp,
105+
getYargsCompletions,
96106
...rest,
97107
},
98108
},
@@ -111,9 +121,16 @@ export async function runCommand(args: string[], logger: logging.Logger): Promis
111121
localYargs = addCommandModuleToYargs(localYargs, CommandModule, context);
112122
}
113123

124+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
125+
const usageInstance = (localYargs as any).getInternalMethods().getUsageInstance();
114126
if (jsonHelp) {
115-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
116-
(localYargs as any).getInternalMethods().getUsageInstance().help = () => jsonHelpUsage();
127+
usageInstance.help = () => jsonHelpUsage();
128+
}
129+
130+
if (getYargsCompletions) {
131+
// When in auto completion mode avoid printing description as it causes a slugish
132+
// experience when there are a large set of options.
133+
usageInstance.getDescriptions = () => ({});
117134
}
118135

119136
await localYargs
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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 { join } from 'path';
10+
import yargs, { Argv } from 'yargs';
11+
import { CommandModule, CommandModuleImplementation } from '../../command-builder/command-module';
12+
13+
export class CompletionCommandModule extends CommandModule implements CommandModuleImplementation {
14+
command = 'completion';
15+
describe = 'Generate a bash and zsh real-time type-ahead autocompletion script.';
16+
longDescriptionPath = join(__dirname, 'long-description.md');
17+
18+
builder(localYargs: Argv): Argv {
19+
return localYargs;
20+
}
21+
22+
run(): void {
23+
yargs.showCompletionScript();
24+
}
25+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
To enable bash and zsh real-time type-ahead autocompletion, copy and paste the generated script to your `.bashrc`, `.bash_profile`, `.zshrc` or `.zsh_profile`.

packages/angular/cli/src/commands/run/cli.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import { join } from 'path';
1111
import { Argv } from 'yargs';
1212
import { ArchitectBaseCommandModule } from '../../command-builder/architect-base-command-module';
1313
import {
14-
CommandModule,
1514
CommandModuleError,
1615
CommandModuleImplementation,
1716
CommandScope,
@@ -35,11 +34,16 @@ export class RunCommandModule
3534
longDescriptionPath = join(__dirname, 'long-description.md');
3635

3736
async builder(argv: Argv): Promise<Argv<RunCommandArgs>> {
37+
const { jsonHelp, getYargsCompletions, help } = this.context.args.options;
38+
3839
const localYargs: Argv<RunCommandArgs> = argv
3940
.positional('target', {
4041
describe: 'The Architect target to run.',
4142
type: 'string',
4243
demandOption: true,
44+
// Show only in when using --help and auto completion because otherwise comma seperated configuration values will be invalid.
45+
// Also, hide choices from JSON help so that we don't display them in AIO.
46+
choices: (getYargsCompletions || help) && !jsonHelp ? this.getTargetChoices() : undefined,
4347
})
4448
.strict();
4549

@@ -78,4 +82,29 @@ export class RunCommandModule
7882
configuration,
7983
};
8084
}
85+
86+
/** @returns a sorted list of target specifiers to be used for auto completion. */
87+
private getTargetChoices(): string[] | undefined {
88+
if (!this.context.workspace) {
89+
return;
90+
}
91+
92+
const targets = [];
93+
for (const [projectName, project] of this.context.workspace.projects) {
94+
for (const [targetName, target] of project.targets) {
95+
const currentTarget = `${projectName}:${targetName}`;
96+
targets.push(currentTarget);
97+
98+
if (!target.configurations) {
99+
continue;
100+
}
101+
102+
for (const configName of Object.keys(target.configurations)) {
103+
targets.push(`${currentTarget}:${configName}`);
104+
}
105+
}
106+
}
107+
108+
return targets.sort();
109+
}
81110
}

tests/legacy-cli/e2e/tests/misc/browsers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import express from 'express';
22
import * as path from 'path';
33
import { copyProjectAsset } from '../../utils/assets';
4-
import { appendToFile, replaceInFile } from '../../utils/fs';
4+
import { replaceInFile } from '../../utils/fs';
55
import { ng } from '../../utils/process';
66

77
export default async function () {
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { execAndWaitForOutputToMatch } from '../../utils/process';
2+
3+
export default async function () {
4+
// ng build
5+
await execAndWaitForOutputToMatch('ng', ['--get-yargs-completions', 'b', ''], /test-project/);
6+
await execAndWaitForOutputToMatch('ng', ['--get-yargs-completions', 'build', ''], /test-project/);
7+
await execAndWaitForOutputToMatch('ng', ['--get-yargs-completions', 'build', '--a'], /--aot/);
8+
await execAndWaitForOutputToMatch(
9+
'ng',
10+
['--get-yargs-completions', 'build', '--configuration'],
11+
/production/,
12+
);
13+
await execAndWaitForOutputToMatch(
14+
'ng',
15+
['--get-yargs-completions', 'b', '--configuration'],
16+
/production/,
17+
);
18+
19+
// ng run
20+
await execAndWaitForOutputToMatch(
21+
'ng',
22+
['--get-yargs-completions', 'run', ''],
23+
/test-project\:build\:development/,
24+
);
25+
await execAndWaitForOutputToMatch(
26+
'ng',
27+
['--get-yargs-completions', 'run', ''],
28+
/test-project\:build/,
29+
);
30+
await execAndWaitForOutputToMatch(
31+
'ng',
32+
['--get-yargs-completions', 'run', ''],
33+
/test-project\:test/,
34+
);
35+
await execAndWaitForOutputToMatch(
36+
'ng',
37+
['--get-yargs-completions', 'run', 'test-project:build'],
38+
/test-project\:build\:development/,
39+
);
40+
await execAndWaitForOutputToMatch(
41+
'ng',
42+
['--get-yargs-completions', 'run', 'test-project:'],
43+
/test-project\:test/,
44+
);
45+
await execAndWaitForOutputToMatch(
46+
'ng',
47+
['--get-yargs-completions', 'run', 'test-project:build'],
48+
// does not include 'test-project:serve'
49+
/^((?!:serve).)*$/,
50+
);
51+
}

0 commit comments

Comments
 (0)