Skip to content

Commit 82ec1af

Browse files
committed
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.
1 parent 14929e2 commit 82ec1af

File tree

15 files changed

+107
-75
lines changed

15 files changed

+107
-75
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export abstract class ArchitectBaseCommandModule<T extends object>
3737
extends CommandModule<T>
3838
implements CommandModuleImplementation<T>
3939
{
40-
static override scope = CommandScope.In;
40+
override scope = CommandScope.In;
4141
protected override shouldReportAnalytics = false;
4242
protected readonly missingTargetChoices: MissingTargetChoice[] | undefined;
4343

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ export type OtherOptions = Record<string, unknown>;
5858

5959
export interface CommandModuleImplementation<T extends {} = {}>
6060
extends Omit<YargsCommandModule<{}, T>, 'builder' | 'handler'> {
61+
/** Scope in which the command can be executed in. */
62+
scope: CommandScope;
6163
/** Path used to load the long description for the command in JSON help text. */
6264
longDescriptionPath?: string;
6365
/** 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
7779
abstract readonly describe: string | false;
7880
abstract readonly longDescriptionPath?: string;
7981
protected readonly shouldReportAnalytics: boolean = true;
80-
static scope = CommandScope.Both;
82+
readonly scope: CommandScope = CommandScope.Both;
8183

8284
private readonly optionsWithAnalytics = new Map<string, number>();
8385

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

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -112,14 +112,6 @@ export async function runCommand(args: string[], logger: logging.Logger): Promis
112112

113113
let localYargs = yargs(args);
114114
for (const CommandModule of COMMANDS) {
115-
if (!jsonHelp) {
116-
// Skip scope validation when running with '--json-help' since it's easier to generate the output for all commands this way.
117-
const scope = CommandModule.scope;
118-
if ((scope === CommandScope.In && !workspace) || (scope === CommandScope.Out && workspace)) {
119-
continue;
120-
}
121-
}
122-
123115
localYargs = addCommandModuleToYargs(localYargs, CommandModule, context);
124116
}
125117

@@ -157,7 +149,7 @@ export async function runCommand(args: string[], logger: logging.Logger): Promis
157149
'deprecated: %s': colors.yellow('deprecated:') + ' %s',
158150
'Did you mean %s?': 'Unknown command. Did you mean %s?',
159151
})
160-
.epilogue(colors.gray(getEpilogue(!!workspace)))
152+
.epilogue(colors.gray('For more information, see https://angular.io/cli/.\n'))
161153
.demandCommand(1, demandCommandFailureMessage)
162154
.recommendCommands()
163155
.middleware(normalizeOptionsMiddleware)
@@ -176,18 +168,3 @@ export async function runCommand(args: string[], logger: logging.Logger): Promis
176168

177169
return process.exitCode ?? 0;
178170
}
179-
180-
function getEpilogue(isInsideWorkspace: boolean): string {
181-
let message: string;
182-
if (isInsideWorkspace) {
183-
message =
184-
'The above commands are available when running the Angular CLI inside a workspace.' +
185-
'More commands are available when running outside a workspace.\n';
186-
} else {
187-
message =
188-
'The above commands are available when running the Angular CLI outside a workspace.' +
189-
'More commands are available when running inside a workspace.\n';
190-
}
191-
192-
return message + 'For more information, see https://angular.io/cli/.\n';
193-
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export abstract class SchematicsCommandModule
4848
extends CommandModule<SchematicsCommandArgs>
4949
implements CommandModuleImplementation<SchematicsCommandArgs>
5050
{
51-
static override scope = CommandScope.In;
51+
override scope = CommandScope.In;
5252
protected readonly allowPrivateSchematics: boolean = false;
5353
protected override readonly shouldReportAnalytics = false;
5454

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

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

99
import { Argv } from 'yargs';
10-
import { CommandContext, CommandModule, CommandModuleImplementation } from '../command-module';
10+
import {
11+
CommandContext,
12+
CommandModule,
13+
CommandModuleError,
14+
CommandModuleImplementation,
15+
CommandScope,
16+
} from '../command-module';
1117

1218
export const demandCommandFailureMessage = `You need to specify a command before moving on. Use '--help' to view the available commands.`;
1319

@@ -18,7 +24,14 @@ export function addCommandModuleToYargs<
1824
},
1925
>(localYargs: Argv<T>, commandModule: U, context: CommandContext): Argv<T> {
2026
const cmd = new commandModule(context);
21-
const describe = context.args.options.jsonHelp ? cmd.fullDescribe : cmd.describe;
27+
const {
28+
args: {
29+
options: { jsonHelp },
30+
},
31+
workspace,
32+
} = context;
33+
34+
const describe = jsonHelp ? cmd.fullDescribe : cmd.describe;
2235

2336
return localYargs.command({
2437
command: cmd.command,
@@ -28,7 +41,23 @@ export function addCommandModuleToYargs<
2841
// Therefore, we get around this by adding a complex object as a string which we later parse when generating the help files.
2942
typeof describe === 'object' ? JSON.stringify(describe) : describe,
3043
deprecated: cmd.deprecated,
31-
builder: (argv) => cmd.builder(argv) as Argv<T>,
44+
builder: (argv) => {
45+
// Skip scope validation when running with '--json-help' since it's easier to generate the output for all commands this way.
46+
const isInvalidScope =
47+
!jsonHelp &&
48+
((cmd.scope === CommandScope.In && !workspace) ||
49+
(cmd.scope === CommandScope.Out && workspace));
50+
51+
if (isInvalidScope) {
52+
throw new CommandModuleError(
53+
`This command is not available when running the Angular CLI ${
54+
workspace ? 'inside' : 'outside'
55+
} a workspace.`,
56+
);
57+
}
58+
59+
return cmd.builder(argv) as Argv<T>;
60+
},
3261
handler: (args) => cmd.handler(args),
3362
});
3463
}

packages/angular/cli/src/commands/cache/clean/cli.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export class CacheCleanModule extends CommandModule implements CommandModuleImpl
1919
command = 'clean';
2020
describe = 'Deletes persistent disk cache from disk.';
2121
longDescriptionPath: string | undefined;
22-
static override scope = CommandScope.In;
22+
override scope = CommandScope.In;
2323

2424
builder(localYargs: Argv): Argv {
2525
return localYargs.strict();

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export class CacheCommandModule extends CommandModule implements CommandModuleIm
2626
command = 'cache';
2727
describe = 'Configure persistent disk cache and retrieve cache statistics.';
2828
longDescriptionPath = join(__dirname, 'long-description.md');
29-
static override scope = CommandScope.In;
29+
override scope = CommandScope.In;
3030

3131
builder(localYargs: Argv): Argv {
3232
const subcommands = [

packages/angular/cli/src/commands/cache/info/cli.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export class CacheInfoCommandModule extends CommandModule implements CommandModu
2222
command = 'info';
2323
describe = 'Prints persistent disk cache configuration and statistics in the console.';
2424
longDescriptionPath?: string | undefined;
25-
static override scope = CommandScope.In;
25+
override scope = CommandScope.In;
2626

2727
builder(localYargs: Argv): Argv {
2828
return localYargs.strict();

packages/angular/cli/src/commands/cache/settings/cli.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export class CacheDisableModule extends CommandModule implements CommandModuleIm
1919
aliases = 'off';
2020
describe = 'Disables persistent disk cache for all projects in the workspace.';
2121
longDescriptionPath: string | undefined;
22-
static override scope = CommandScope.In;
22+
override scope = CommandScope.In;
2323

2424
builder(localYargs: Argv): Argv {
2525
return localYargs;
@@ -35,7 +35,7 @@ export class CacheEnableModule extends CommandModule implements CommandModuleImp
3535
aliases = 'on';
3636
describe = 'Enables disk cache for all projects in the workspace.';
3737
longDescriptionPath: string | undefined;
38-
static override scope = CommandScope.In;
38+
override scope = CommandScope.In;
3939

4040
builder(localYargs: Argv): Argv {
4141
return localYargs;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export class NewCommandModule
2929
implements CommandModuleImplementation<NewCommandArgs>
3030
{
3131
private readonly schematicName = 'ng-new';
32-
static override scope = CommandScope.Out;
32+
override scope = CommandScope.Out;
3333
protected override allowPrivateSchematics = true;
3434

3535
command = 'new [name]';

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export class RunCommandModule
2626
extends ArchitectBaseCommandModule<RunCommandArgs>
2727
implements CommandModuleImplementation<RunCommandArgs>
2828
{
29-
static override scope = CommandScope.In;
29+
override scope = CommandScope.In;
3030

3131
command = 'run <target>';
3232
describe =

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ const ANGULAR_PACKAGES_REGEXP = /^@(?:angular|nguniversal)\//;
6060
const UPDATE_SCHEMATIC_COLLECTION = path.join(__dirname, 'schematic/collection.json');
6161

6262
export class UpdateCommandModule extends CommandModule<UpdateCommandArgs> {
63-
static override scope = CommandScope.In;
63+
override scope = CommandScope.In;
6464
protected override shouldReportAnalytics = false;
6565

6666
command = 'update [packages..]';
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { homedir } from 'os';
2+
import { silentNg } from '../../utils/process';
3+
import { expectToFail } from '../../utils/utils';
4+
5+
export default async function () {
6+
const originalCwd = process.cwd();
7+
8+
try {
9+
// Run inside workspace
10+
await silentNg('generate', 'component', 'foo', '--dry-run');
11+
12+
// The version command can be run in and outside of a workspace.
13+
await silentNg('version');
14+
15+
const { message: ngNewFailure } = await expectToFail(() =>
16+
silentNg('new', 'proj-name', '--dry-run'),
17+
);
18+
if (
19+
!ngNewFailure.includes(
20+
'This command is not available when running the Angular CLI inside a workspace.',
21+
)
22+
) {
23+
throw new Error('ng new should have failed when ran inside a workspace.');
24+
}
25+
26+
// Chnage CWD to run outside a workspace.
27+
process.chdir(homedir());
28+
29+
// ng generate can only be ran inside.
30+
const { message: ngGenerateFailure } = await expectToFail(() =>
31+
silentNg('generate', 'component', 'foo', '--dry-run'),
32+
);
33+
if (
34+
!ngGenerateFailure.includes(
35+
'This command is not available when running the Angular CLI outside a workspace.',
36+
)
37+
) {
38+
throw new Error('ng generate should have failed when ran outside a workspace.');
39+
}
40+
41+
// ng new can only be ran outside of a workspace
42+
await silentNg('new', 'proj-name', '--dry-run');
43+
44+
// The version command can be run in and outside of a workspace.
45+
await silentNg('version');
46+
} finally {
47+
process.chdir(originalCwd);
48+
}
49+
}

tests/legacy-cli/e2e/tests/basic/in-project-logic.ts

Lines changed: 0 additions & 21 deletions
This file was deleted.
Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,15 @@
11
import { silentNg } from '../../../utils/process';
22

3-
export default function () {
4-
return Promise.resolve()
5-
.then(() => silentNg('--help'))
6-
.then(({ stdout }) => {
7-
if (stdout.match(/(easter-egg)|(ng make-this-awesome)|(ng init)/)) {
8-
throw new Error(
9-
'Expected to not match "(easter-egg)|(ng make-this-awesome)|(ng init)" in help output.',
10-
);
11-
}
12-
})
13-
.then(() => silentNg('--help', 'new'))
14-
.then(({ stdout }) => {
15-
if (stdout.match(/--link-cli/)) {
16-
throw new Error('Expected to not match "--link-cli" in help output.');
17-
}
18-
});
3+
export default async function () {
4+
const { stdout: stdoutNew } = await silentNg('--help');
5+
if (/(easter-egg)|(ng make-this-awesome)|(ng init)/.test(stdoutNew)) {
6+
throw new Error(
7+
'Expected to not match "(easter-egg)|(ng make-this-awesome)|(ng init)" in help output.',
8+
);
9+
}
10+
11+
const { stdout: ngGenerate } = await silentNg('--help', 'generate', 'component');
12+
if (ngGenerate.includes('--path')) {
13+
throw new Error('Expected to not match "--path" in help output.');
14+
}
1915
}

0 commit comments

Comments
 (0)