diff --git a/packages/angular/cli/src/command-builder/architect-base-command-module.ts b/packages/angular/cli/src/command-builder/architect-base-command-module.ts index 676dd243f8..dd83af5d92 100644 --- a/packages/angular/cli/src/command-builder/architect-base-command-module.ts +++ b/packages/angular/cli/src/command-builder/architect-base-command-module.ts @@ -37,7 +37,7 @@ export abstract class ArchitectBaseCommandModule extends CommandModule implements CommandModuleImplementation { - static override scope = CommandScope.In; + override scope = CommandScope.In; protected override shouldReportAnalytics = false; protected readonly missingTargetChoices: MissingTargetChoice[] | undefined; diff --git a/packages/angular/cli/src/command-builder/command-module.ts b/packages/angular/cli/src/command-builder/command-module.ts index b476c392dd..c8b957890e 100644 --- a/packages/angular/cli/src/command-builder/command-module.ts +++ b/packages/angular/cli/src/command-builder/command-module.ts @@ -58,6 +58,8 @@ export type OtherOptions = Record; export interface CommandModuleImplementation extends Omit, '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 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(); diff --git a/packages/angular/cli/src/command-builder/command-runner.ts b/packages/angular/cli/src/command-builder/command-runner.ts index de8c9187ba..6c3ecdee19 100644 --- a/packages/angular/cli/src/command-builder/command-runner.ts +++ b/packages/angular/cli/src/command-builder/command-runner.ts @@ -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'; -} diff --git a/packages/angular/cli/src/command-builder/schematics-command-module.ts b/packages/angular/cli/src/command-builder/schematics-command-module.ts index d80dc26d25..eb7b0a2601 100644 --- a/packages/angular/cli/src/command-builder/schematics-command-module.ts +++ b/packages/angular/cli/src/command-builder/schematics-command-module.ts @@ -48,7 +48,7 @@ export abstract class SchematicsCommandModule extends CommandModule implements CommandModuleImplementation { - static override scope = CommandScope.In; + override scope = CommandScope.In; protected readonly allowPrivateSchematics: boolean = false; protected override readonly shouldReportAnalytics = false; diff --git a/packages/angular/cli/src/command-builder/utilities/command.ts b/packages/angular/cli/src/command-builder/utilities/command.ts index 525cdcc256..3c3a1fa566 100644 --- a/packages/angular/cli/src/command-builder/utilities/command.ts +++ b/packages/angular/cli/src/command-builder/utilities/command.ts @@ -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, commandModule: U, context: CommandContext): Argv { 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, + 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; + }, handler: (args) => cmd.handler(args), }); } diff --git a/packages/angular/cli/src/commands/cache/clean/cli.ts b/packages/angular/cli/src/commands/cache/clean/cli.ts index f020bde125..f07cd5613c 100644 --- a/packages/angular/cli/src/commands/cache/clean/cli.ts +++ b/packages/angular/cli/src/commands/cache/clean/cli.ts @@ -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(); diff --git a/packages/angular/cli/src/commands/cache/cli.ts b/packages/angular/cli/src/commands/cache/cli.ts index 8ed75a8ca3..f30c4acd3b 100644 --- a/packages/angular/cli/src/commands/cache/cli.ts +++ b/packages/angular/cli/src/commands/cache/cli.ts @@ -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 = [ diff --git a/packages/angular/cli/src/commands/cache/info/cli.ts b/packages/angular/cli/src/commands/cache/info/cli.ts index 186eae3621..15fcf3ba85 100644 --- a/packages/angular/cli/src/commands/cache/info/cli.ts +++ b/packages/angular/cli/src/commands/cache/info/cli.ts @@ -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(); diff --git a/packages/angular/cli/src/commands/cache/settings/cli.ts b/packages/angular/cli/src/commands/cache/settings/cli.ts index 00d56ee997..97e79cd100 100644 --- a/packages/angular/cli/src/commands/cache/settings/cli.ts +++ b/packages/angular/cli/src/commands/cache/settings/cli.ts @@ -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; diff --git a/packages/angular/cli/src/commands/new/cli.ts b/packages/angular/cli/src/commands/new/cli.ts index d93b701ccd..1d4b39f3e4 100644 --- a/packages/angular/cli/src/commands/new/cli.ts +++ b/packages/angular/cli/src/commands/new/cli.ts @@ -29,7 +29,7 @@ export class NewCommandModule implements CommandModuleImplementation { private readonly schematicName = 'ng-new'; - static override scope = CommandScope.Out; + override scope = CommandScope.Out; protected override allowPrivateSchematics = true; command = 'new [name]'; diff --git a/packages/angular/cli/src/commands/run/cli.ts b/packages/angular/cli/src/commands/run/cli.ts index 3d6f31fdfa..963a70e646 100644 --- a/packages/angular/cli/src/commands/run/cli.ts +++ b/packages/angular/cli/src/commands/run/cli.ts @@ -26,7 +26,7 @@ export class RunCommandModule extends ArchitectBaseCommandModule implements CommandModuleImplementation { - static override scope = CommandScope.In; + override scope = CommandScope.In; command = 'run '; describe = diff --git a/packages/angular/cli/src/commands/update/cli.ts b/packages/angular/cli/src/commands/update/cli.ts index 91f4613ef8..152c6d3b86 100644 --- a/packages/angular/cli/src/commands/update/cli.ts +++ b/packages/angular/cli/src/commands/update/cli.ts @@ -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 { - static override scope = CommandScope.In; + override scope = CommandScope.In; protected override shouldReportAnalytics = false; command = 'update [packages..]'; diff --git a/tests/legacy-cli/e2e/tests/basic/command-scope.ts b/tests/legacy-cli/e2e/tests/basic/command-scope.ts new file mode 100644 index 0000000000..94c91e1693 --- /dev/null +++ b/tests/legacy-cli/e2e/tests/basic/command-scope.ts @@ -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); + } +} diff --git a/tests/legacy-cli/e2e/tests/basic/in-project-logic.ts b/tests/legacy-cli/e2e/tests/basic/in-project-logic.ts deleted file mode 100644 index 7586ba4392..0000000000 --- a/tests/legacy-cli/e2e/tests/basic/in-project-logic.ts +++ /dev/null @@ -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'))) - ); -} diff --git a/tests/legacy-cli/e2e/tests/commands/help/help-hidden.ts b/tests/legacy-cli/e2e/tests/commands/help/help-hidden.ts index d3b72c39e2..bf61603960 100644 --- a/tests/legacy-cli/e2e/tests/commands/help/help-hidden.ts +++ b/tests/legacy-cli/e2e/tests/commands/help/help-hidden.ts @@ -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)/)) { - 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.'); - } - }); +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.', + ); + } + + const { stdout: ngGenerate } = await silentNg('--help', 'generate', 'component'); + if (ngGenerate.includes('--path')) { + throw new Error('Expected to not match "--path" in help output.'); + } }