mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-17 02:54:21 +08:00
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:
parent
95954bba04
commit
607a723f7d
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -48,6 +48,7 @@ export interface CommandContext {
|
||||
options: {
|
||||
help: boolean;
|
||||
jsonHelp: boolean;
|
||||
getYargsCompletions: boolean;
|
||||
} & Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
@ -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
|
||||
|
25
packages/angular/cli/src/commands/completion/cli.ts
Normal file
25
packages/angular/cli/src/commands/completion/cli.ts
Normal 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();
|
||||
}
|
||||
}
|
@ -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`.
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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 () {
|
||||
|
51
tests/legacy-cli/e2e/tests/misc/completion.ts
Normal file
51
tests/legacy-cli/e2e/tests/misc/completion.ts
Normal 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).)*$/,
|
||||
);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user