mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-16 10:33:43 +08:00
fix(@angular/cli): show more actionable error when command is ran in wrong scope
Currently, we don't register all available commands. For instance, when the CLI is ran inside a workspace the `new` command is not registered. Thus, this will cause a confusing error message when `ng new` is ran inside a workspace. Example: ``` $ ng new Error: Unknown command. Did you mean e? ``` With this commit we change this by registering all the commands and valid the command scope during the command building phase which is only triggered once the command is invoked but prior to the execution phase.
This commit is contained in:
parent
14929e28b1
commit
82ec1af4e1
@ -37,7 +37,7 @@ export abstract class ArchitectBaseCommandModule<T extends object>
|
||||
extends CommandModule<T>
|
||||
implements CommandModuleImplementation<T>
|
||||
{
|
||||
static override scope = CommandScope.In;
|
||||
override scope = CommandScope.In;
|
||||
protected override shouldReportAnalytics = false;
|
||||
protected readonly missingTargetChoices: MissingTargetChoice[] | undefined;
|
||||
|
||||
|
@ -58,6 +58,8 @@ export type OtherOptions = Record<string, unknown>;
|
||||
|
||||
export interface CommandModuleImplementation<T extends {} = {}>
|
||||
extends Omit<YargsCommandModule<{}, T>, 'builder' | 'handler'> {
|
||||
/** Scope in which the command can be executed in. */
|
||||
scope: CommandScope;
|
||||
/** Path used to load the long description for the command in JSON help text. */
|
||||
longDescriptionPath?: string;
|
||||
/** Object declaring the options the command accepts, or a function accepting and returning a yargs instance. */
|
||||
@ -77,7 +79,7 @@ export abstract class CommandModule<T extends {} = {}> implements CommandModuleI
|
||||
abstract readonly describe: string | false;
|
||||
abstract readonly longDescriptionPath?: string;
|
||||
protected readonly shouldReportAnalytics: boolean = true;
|
||||
static scope = CommandScope.Both;
|
||||
readonly scope: CommandScope = CommandScope.Both;
|
||||
|
||||
private readonly optionsWithAnalytics = new Map<string, number>();
|
||||
|
||||
|
@ -112,14 +112,6 @@ export async function runCommand(args: string[], logger: logging.Logger): Promis
|
||||
|
||||
let localYargs = yargs(args);
|
||||
for (const CommandModule of COMMANDS) {
|
||||
if (!jsonHelp) {
|
||||
// Skip scope validation when running with '--json-help' since it's easier to generate the output for all commands this way.
|
||||
const scope = CommandModule.scope;
|
||||
if ((scope === CommandScope.In && !workspace) || (scope === CommandScope.Out && workspace)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
localYargs = addCommandModuleToYargs(localYargs, CommandModule, context);
|
||||
}
|
||||
|
||||
@ -157,7 +149,7 @@ export async function runCommand(args: string[], logger: logging.Logger): Promis
|
||||
'deprecated: %s': colors.yellow('deprecated:') + ' %s',
|
||||
'Did you mean %s?': 'Unknown command. Did you mean %s?',
|
||||
})
|
||||
.epilogue(colors.gray(getEpilogue(!!workspace)))
|
||||
.epilogue(colors.gray('For more information, see https://angular.io/cli/.\n'))
|
||||
.demandCommand(1, demandCommandFailureMessage)
|
||||
.recommendCommands()
|
||||
.middleware(normalizeOptionsMiddleware)
|
||||
@ -176,18 +168,3 @@ export async function runCommand(args: string[], logger: logging.Logger): Promis
|
||||
|
||||
return process.exitCode ?? 0;
|
||||
}
|
||||
|
||||
function getEpilogue(isInsideWorkspace: boolean): string {
|
||||
let message: string;
|
||||
if (isInsideWorkspace) {
|
||||
message =
|
||||
'The above commands are available when running the Angular CLI inside a workspace.' +
|
||||
'More commands are available when running outside a workspace.\n';
|
||||
} else {
|
||||
message =
|
||||
'The above commands are available when running the Angular CLI outside a workspace.' +
|
||||
'More commands are available when running inside a workspace.\n';
|
||||
}
|
||||
|
||||
return message + 'For more information, see https://angular.io/cli/.\n';
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ export abstract class SchematicsCommandModule
|
||||
extends CommandModule<SchematicsCommandArgs>
|
||||
implements CommandModuleImplementation<SchematicsCommandArgs>
|
||||
{
|
||||
static override scope = CommandScope.In;
|
||||
override scope = CommandScope.In;
|
||||
protected readonly allowPrivateSchematics: boolean = false;
|
||||
protected override readonly shouldReportAnalytics = false;
|
||||
|
||||
|
@ -7,7 +7,13 @@
|
||||
*/
|
||||
|
||||
import { Argv } from 'yargs';
|
||||
import { CommandContext, CommandModule, CommandModuleImplementation } from '../command-module';
|
||||
import {
|
||||
CommandContext,
|
||||
CommandModule,
|
||||
CommandModuleError,
|
||||
CommandModuleImplementation,
|
||||
CommandScope,
|
||||
} from '../command-module';
|
||||
|
||||
export const demandCommandFailureMessage = `You need to specify a command before moving on. Use '--help' to view the available commands.`;
|
||||
|
||||
@ -18,7 +24,14 @@ export function addCommandModuleToYargs<
|
||||
},
|
||||
>(localYargs: Argv<T>, commandModule: U, context: CommandContext): Argv<T> {
|
||||
const cmd = new commandModule(context);
|
||||
const describe = context.args.options.jsonHelp ? cmd.fullDescribe : cmd.describe;
|
||||
const {
|
||||
args: {
|
||||
options: { jsonHelp },
|
||||
},
|
||||
workspace,
|
||||
} = context;
|
||||
|
||||
const describe = jsonHelp ? cmd.fullDescribe : cmd.describe;
|
||||
|
||||
return localYargs.command({
|
||||
command: cmd.command,
|
||||
@ -28,7 +41,23 @@ export function addCommandModuleToYargs<
|
||||
// Therefore, we get around this by adding a complex object as a string which we later parse when generating the help files.
|
||||
typeof describe === 'object' ? JSON.stringify(describe) : describe,
|
||||
deprecated: cmd.deprecated,
|
||||
builder: (argv) => cmd.builder(argv) as Argv<T>,
|
||||
builder: (argv) => {
|
||||
// Skip scope validation when running with '--json-help' since it's easier to generate the output for all commands this way.
|
||||
const isInvalidScope =
|
||||
!jsonHelp &&
|
||||
((cmd.scope === CommandScope.In && !workspace) ||
|
||||
(cmd.scope === CommandScope.Out && workspace));
|
||||
|
||||
if (isInvalidScope) {
|
||||
throw new CommandModuleError(
|
||||
`This command is not available when running the Angular CLI ${
|
||||
workspace ? 'inside' : 'outside'
|
||||
} a workspace.`,
|
||||
);
|
||||
}
|
||||
|
||||
return cmd.builder(argv) as Argv<T>;
|
||||
},
|
||||
handler: (args) => cmd.handler(args),
|
||||
});
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ export class CacheCleanModule extends CommandModule implements CommandModuleImpl
|
||||
command = 'clean';
|
||||
describe = 'Deletes persistent disk cache from disk.';
|
||||
longDescriptionPath: string | undefined;
|
||||
static override scope = CommandScope.In;
|
||||
override scope = CommandScope.In;
|
||||
|
||||
builder(localYargs: Argv): Argv {
|
||||
return localYargs.strict();
|
||||
|
@ -26,7 +26,7 @@ export class CacheCommandModule extends CommandModule implements CommandModuleIm
|
||||
command = 'cache';
|
||||
describe = 'Configure persistent disk cache and retrieve cache statistics.';
|
||||
longDescriptionPath = join(__dirname, 'long-description.md');
|
||||
static override scope = CommandScope.In;
|
||||
override scope = CommandScope.In;
|
||||
|
||||
builder(localYargs: Argv): Argv {
|
||||
const subcommands = [
|
||||
|
@ -22,7 +22,7 @@ export class CacheInfoCommandModule extends CommandModule implements CommandModu
|
||||
command = 'info';
|
||||
describe = 'Prints persistent disk cache configuration and statistics in the console.';
|
||||
longDescriptionPath?: string | undefined;
|
||||
static override scope = CommandScope.In;
|
||||
override scope = CommandScope.In;
|
||||
|
||||
builder(localYargs: Argv): Argv {
|
||||
return localYargs.strict();
|
||||
|
@ -19,7 +19,7 @@ export class CacheDisableModule extends CommandModule implements CommandModuleIm
|
||||
aliases = 'off';
|
||||
describe = 'Disables persistent disk cache for all projects in the workspace.';
|
||||
longDescriptionPath: string | undefined;
|
||||
static override scope = CommandScope.In;
|
||||
override scope = CommandScope.In;
|
||||
|
||||
builder(localYargs: Argv): Argv {
|
||||
return localYargs;
|
||||
@ -35,7 +35,7 @@ export class CacheEnableModule extends CommandModule implements CommandModuleImp
|
||||
aliases = 'on';
|
||||
describe = 'Enables disk cache for all projects in the workspace.';
|
||||
longDescriptionPath: string | undefined;
|
||||
static override scope = CommandScope.In;
|
||||
override scope = CommandScope.In;
|
||||
|
||||
builder(localYargs: Argv): Argv {
|
||||
return localYargs;
|
||||
|
@ -29,7 +29,7 @@ export class NewCommandModule
|
||||
implements CommandModuleImplementation<NewCommandArgs>
|
||||
{
|
||||
private readonly schematicName = 'ng-new';
|
||||
static override scope = CommandScope.Out;
|
||||
override scope = CommandScope.Out;
|
||||
protected override allowPrivateSchematics = true;
|
||||
|
||||
command = 'new [name]';
|
||||
|
@ -26,7 +26,7 @@ export class RunCommandModule
|
||||
extends ArchitectBaseCommandModule<RunCommandArgs>
|
||||
implements CommandModuleImplementation<RunCommandArgs>
|
||||
{
|
||||
static override scope = CommandScope.In;
|
||||
override scope = CommandScope.In;
|
||||
|
||||
command = 'run <target>';
|
||||
describe =
|
||||
|
@ -60,7 +60,7 @@ const ANGULAR_PACKAGES_REGEXP = /^@(?:angular|nguniversal)\//;
|
||||
const UPDATE_SCHEMATIC_COLLECTION = path.join(__dirname, 'schematic/collection.json');
|
||||
|
||||
export class UpdateCommandModule extends CommandModule<UpdateCommandArgs> {
|
||||
static override scope = CommandScope.In;
|
||||
override scope = CommandScope.In;
|
||||
protected override shouldReportAnalytics = false;
|
||||
|
||||
command = 'update [packages..]';
|
||||
|
49
tests/legacy-cli/e2e/tests/basic/command-scope.ts
Normal file
49
tests/legacy-cli/e2e/tests/basic/command-scope.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { homedir } from 'os';
|
||||
import { silentNg } from '../../utils/process';
|
||||
import { expectToFail } from '../../utils/utils';
|
||||
|
||||
export default async function () {
|
||||
const originalCwd = process.cwd();
|
||||
|
||||
try {
|
||||
// Run inside workspace
|
||||
await silentNg('generate', 'component', 'foo', '--dry-run');
|
||||
|
||||
// The version command can be run in and outside of a workspace.
|
||||
await silentNg('version');
|
||||
|
||||
const { message: ngNewFailure } = await expectToFail(() =>
|
||||
silentNg('new', 'proj-name', '--dry-run'),
|
||||
);
|
||||
if (
|
||||
!ngNewFailure.includes(
|
||||
'This command is not available when running the Angular CLI inside a workspace.',
|
||||
)
|
||||
) {
|
||||
throw new Error('ng new should have failed when ran inside a workspace.');
|
||||
}
|
||||
|
||||
// Chnage CWD to run outside a workspace.
|
||||
process.chdir(homedir());
|
||||
|
||||
// ng generate can only be ran inside.
|
||||
const { message: ngGenerateFailure } = await expectToFail(() =>
|
||||
silentNg('generate', 'component', 'foo', '--dry-run'),
|
||||
);
|
||||
if (
|
||||
!ngGenerateFailure.includes(
|
||||
'This command is not available when running the Angular CLI outside a workspace.',
|
||||
)
|
||||
) {
|
||||
throw new Error('ng generate should have failed when ran outside a workspace.');
|
||||
}
|
||||
|
||||
// ng new can only be ran outside of a workspace
|
||||
await silentNg('new', 'proj-name', '--dry-run');
|
||||
|
||||
// The version command can be run in and outside of a workspace.
|
||||
await silentNg('version');
|
||||
} finally {
|
||||
process.chdir(originalCwd);
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
import * as os from 'os';
|
||||
import { join } from 'path';
|
||||
import { writeFile, deleteFile } from '../../utils/fs';
|
||||
import { ng } from '../../utils/process';
|
||||
import { expectToFail } from '../../utils/utils';
|
||||
|
||||
export default function () {
|
||||
const homedir = os.homedir();
|
||||
const globalConfigPath = join(homedir, '.angular-config.json');
|
||||
return (
|
||||
Promise.resolve()
|
||||
.then(() => writeFile(globalConfigPath, '{"version":1}'))
|
||||
.then(() => process.chdir(homedir))
|
||||
.then(() => ng('new', 'proj-name', '--dry-run'))
|
||||
.then(() => deleteFile(globalConfigPath))
|
||||
// Test that we cannot create a project inside another project.
|
||||
.then(() => writeFile(join(homedir, '.angular.json'), '{"version":1}'))
|
||||
.then(() => expectToFail(() => ng('new', 'proj-name', '--dry-run')))
|
||||
.then(() => deleteFile(join(homedir, '.angular.json')))
|
||||
);
|
||||
}
|
@ -1,19 +1,15 @@
|
||||
import { silentNg } from '../../../utils/process';
|
||||
|
||||
export default function () {
|
||||
return Promise.resolve()
|
||||
.then(() => silentNg('--help'))
|
||||
.then(({ stdout }) => {
|
||||
if (stdout.match(/(easter-egg)|(ng make-this-awesome)|(ng init)/)) {
|
||||
export default async function () {
|
||||
const { stdout: stdoutNew } = await silentNg('--help');
|
||||
if (/(easter-egg)|(ng make-this-awesome)|(ng init)/.test(stdoutNew)) {
|
||||
throw new Error(
|
||||
'Expected to not match "(easter-egg)|(ng make-this-awesome)|(ng init)" in help output.',
|
||||
);
|
||||
}
|
||||
})
|
||||
.then(() => silentNg('--help', 'new'))
|
||||
.then(({ stdout }) => {
|
||||
if (stdout.match(/--link-cli/)) {
|
||||
throw new Error('Expected to not match "--link-cli" in help output.');
|
||||
|
||||
const { stdout: ngGenerate } = await silentNg('--help', 'generate', 'component');
|
||||
if (ngGenerate.includes('--path')) {
|
||||
throw new Error('Expected to not match "--path" in help output.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user