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
This commit is contained in:
Alan Agius 2022-04-13 18:26:55 +02:00 committed by Douglas Parker
parent 95954bba04
commit 607a723f7d
9 changed files with 174 additions and 15 deletions

View File

@ -7,7 +7,10 @@
*/
import { Architect, Target } from '@angular-devkit/architect';
import { WorkspaceNodeModulesArchitectHost } from '@angular-devkit/architect/node';
import {
NodeModulesBuilderInfo,
WorkspaceNodeModulesArchitectHost,
} from '@angular-devkit/architect/node';
import { json } from '@angular-devkit/core';
import { spawnSync } from 'child_process';
import { existsSync } from 'fs';
@ -100,9 +103,15 @@ export abstract class ArchitectBaseCommandModule<T>
protected async getArchitectTargetOptions(target: Target): Promise<Option[]> {
const architectHost = this.getArchitectHost();
const builderConf = await architectHost.getBuilderNameForTarget(target);
let builderConf: string;
let builderDesc;
try {
builderConf = await architectHost.getBuilderNameForTarget(target);
} catch {
return [];
}
let builderDesc: NodeModulesBuilderInfo;
try {
builderDesc = await architectHost.resolveBuilder(builderConf);
} catch (e) {

View File

@ -29,10 +29,15 @@ export abstract class ArchitectCommandModule
abstract readonly multiTarget: boolean;
async builder(argv: Argv): Promise<Argv<ArchitectCommandArgs>> {
const project = this.getArchitectProject();
const { jsonHelp, getYargsCompletions, help } = this.context.args.options;
const localYargs: Argv<ArchitectCommandArgs> = argv
.positional('project', {
describe: 'The name of the project to build. Can be an application or a library.',
type: 'string',
// Hide choices from JSON help so that we don't display them in AIO.
choices: jsonHelp ? undefined : this.getProjectChoices(),
})
.option('configuration', {
describe:
@ -42,10 +47,15 @@ export abstract class ArchitectCommandModule
`For more information, see https://angular.io/guide/workspace-config#alternate-build-configurations.`,
alias: 'c',
type: 'string',
// Show only in when using --help and auto completion because otherwise comma seperated configuration values will be invalid.
// Also, hide choices from JSON help so that we don't display them in AIO.
choices:
(getYargsCompletions || help) && !jsonHelp && project
? this.getConfigurationChoices(project)
: undefined,
})
.strict();
const project = this.getArchitectProject();
if (!project) {
return localYargs;
}
@ -92,11 +102,7 @@ export abstract class ArchitectCommandModule
const [, projectName] = this.context.args.positional;
if (projectName) {
if (!workspace.projects.has(projectName)) {
throw new CommandModuleError(`Project '${projectName}' does not exist.`);
}
return projectName;
return workspace.projects.has(projectName) ? projectName : undefined;
}
const target = this.getArchitectTarget();
@ -136,4 +142,24 @@ export abstract class ArchitectCommandModule
return undefined;
}
/** @returns a sorted list of project names to be used for auto completion. */
private getProjectChoices(): string[] | undefined {
const { workspace } = this.context;
return workspace ? [...workspace.projects.keys()].sort() : undefined;
}
/** @returns a sorted list of configuration names to be used for auto completion. */
private getConfigurationChoices(project: string): string[] | undefined {
const projectDefinition = this.context.workspace?.projects.get(project);
if (!projectDefinition) {
return undefined;
}
const target = this.getArchitectTarget();
const configurations = projectDefinition.targets.get(target)?.configurations;
return configurations ? Object.keys(configurations).sort() : undefined;
}
}

View File

@ -48,6 +48,7 @@ export interface CommandContext {
options: {
help: boolean;
jsonHelp: boolean;
getYargsCompletions: boolean;
} & Record<string, unknown>;
};
}

View File

@ -13,6 +13,7 @@ import { AddCommandModule } from '../commands/add/cli';
import { AnalyticsCommandModule } from '../commands/analytics/cli';
import { BuildCommandModule } from '../commands/build/cli';
import { CacheCommandModule } from '../commands/cache/cli';
import { CompletionCommandModule } from '../commands/completion/cli';
import { ConfigCommandModule } from '../commands/config/cli';
import { DeployCommandModule } from '../commands/deploy/cli';
import { DocCommandModule } from '../commands/doc/cli';
@ -54,6 +55,7 @@ const COMMANDS = [
UpdateCommandModule,
RunCommandModule,
CacheCommandModule,
CompletionCommandModule,
].sort(); // Will be sorted by class name.
const yargsParser = Parser as unknown as typeof Parser.default;
@ -61,11 +63,18 @@ const yargsParser = Parser as unknown as typeof Parser.default;
export async function runCommand(args: string[], logger: logging.Logger): Promise<number> {
const {
$0,
_: positional,
_,
help = false,
jsonHelp = false,
getYargsCompletions = false,
...rest
} = yargsParser(args, { boolean: ['help', 'json-help'], alias: { 'collection': 'c' } });
} = yargsParser(args, {
boolean: ['help', 'json-help', 'get-yargs-completions'],
alias: { 'collection': 'c' },
});
// When `getYargsCompletions` is true the scriptName 'ng' at index 0 is not removed.
const positional = getYargsCompletions ? _.slice(1) : _;
let workspace: AngularWorkspace | undefined;
let globalConfiguration: AngularWorkspace | undefined;
@ -93,6 +102,7 @@ export async function runCommand(args: string[], logger: logging.Logger): Promis
options: {
help,
jsonHelp,
getYargsCompletions,
...rest,
},
},
@ -111,9 +121,16 @@ export async function runCommand(args: string[], logger: logging.Logger): Promis
localYargs = addCommandModuleToYargs(localYargs, CommandModule, context);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const usageInstance = (localYargs as any).getInternalMethods().getUsageInstance();
if (jsonHelp) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(localYargs as any).getInternalMethods().getUsageInstance().help = () => jsonHelpUsage();
usageInstance.help = () => jsonHelpUsage();
}
if (getYargsCompletions) {
// When in auto completion mode avoid printing description as it causes a slugish
// experience when there are a large set of options.
usageInstance.getDescriptions = () => ({});
}
await localYargs

View File

@ -0,0 +1,25 @@
/**
* @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.io/license
*/
import { join } from 'path';
import yargs, { Argv } from 'yargs';
import { CommandModule, CommandModuleImplementation } from '../../command-builder/command-module';
export class CompletionCommandModule extends CommandModule implements CommandModuleImplementation {
command = 'completion';
describe = 'Generate a bash and zsh real-time type-ahead autocompletion script.';
longDescriptionPath = join(__dirname, 'long-description.md');
builder(localYargs: Argv): Argv {
return localYargs;
}
run(): void {
yargs.showCompletionScript();
}
}

View File

@ -0,0 +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`.

View File

@ -11,7 +11,6 @@ import { join } from 'path';
import { Argv } from 'yargs';
import { ArchitectBaseCommandModule } from '../../command-builder/architect-base-command-module';
import {
CommandModule,
CommandModuleError,
CommandModuleImplementation,
CommandScope,
@ -35,11 +34,16 @@ export class RunCommandModule
longDescriptionPath = join(__dirname, 'long-description.md');
async builder(argv: Argv): Promise<Argv<RunCommandArgs>> {
const { jsonHelp, getYargsCompletions, help } = this.context.args.options;
const localYargs: Argv<RunCommandArgs> = argv
.positional('target', {
describe: 'The Architect target to run.',
type: 'string',
demandOption: true,
// Show only in when using --help and auto completion because otherwise comma seperated configuration values will be invalid.
// Also, hide choices from JSON help so that we don't display them in AIO.
choices: (getYargsCompletions || help) && !jsonHelp ? this.getTargetChoices() : undefined,
})
.strict();
@ -78,4 +82,29 @@ export class RunCommandModule
configuration,
};
}
/** @returns a sorted list of target specifiers to be used for auto completion. */
private getTargetChoices(): string[] | undefined {
if (!this.context.workspace) {
return;
}
const targets = [];
for (const [projectName, project] of this.context.workspace.projects) {
for (const [targetName, target] of project.targets) {
const currentTarget = `${projectName}:${targetName}`;
targets.push(currentTarget);
if (!target.configurations) {
continue;
}
for (const configName of Object.keys(target.configurations)) {
targets.push(`${currentTarget}:${configName}`);
}
}
}
return targets.sort();
}
}

View File

@ -1,7 +1,7 @@
import express from 'express';
import * as path from 'path';
import { copyProjectAsset } from '../../utils/assets';
import { appendToFile, replaceInFile } from '../../utils/fs';
import { replaceInFile } from '../../utils/fs';
import { ng } from '../../utils/process';
export default async function () {

View File

@ -0,0 +1,51 @@
import { execAndWaitForOutputToMatch } from '../../utils/process';
export default async function () {
// ng build
await execAndWaitForOutputToMatch('ng', ['--get-yargs-completions', 'b', ''], /test-project/);
await execAndWaitForOutputToMatch('ng', ['--get-yargs-completions', 'build', ''], /test-project/);
await execAndWaitForOutputToMatch('ng', ['--get-yargs-completions', 'build', '--a'], /--aot/);
await execAndWaitForOutputToMatch(
'ng',
['--get-yargs-completions', 'build', '--configuration'],
/production/,
);
await execAndWaitForOutputToMatch(
'ng',
['--get-yargs-completions', 'b', '--configuration'],
/production/,
);
// ng run
await execAndWaitForOutputToMatch(
'ng',
['--get-yargs-completions', 'run', ''],
/test-project\:build\:development/,
);
await execAndWaitForOutputToMatch(
'ng',
['--get-yargs-completions', 'run', ''],
/test-project\:build/,
);
await execAndWaitForOutputToMatch(
'ng',
['--get-yargs-completions', 'run', ''],
/test-project\:test/,
);
await execAndWaitForOutputToMatch(
'ng',
['--get-yargs-completions', 'run', 'test-project:build'],
/test-project\:build\:development/,
);
await execAndWaitForOutputToMatch(
'ng',
['--get-yargs-completions', 'run', 'test-project:'],
/test-project\:test/,
);
await execAndWaitForOutputToMatch(
'ng',
['--get-yargs-completions', 'run', 'test-project:build'],
// does not include 'test-project:serve'
/^((?!:serve).)*$/,
);
}