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 e2561fd1c273..684a4d9f4b5a 100644 --- a/packages/angular/cli/src/command-builder/schematics-command-module.ts +++ b/packages/angular/cli/src/command-builder/schematics-command-module.ts @@ -123,8 +123,7 @@ export abstract class SchematicsCommandModule optionTransforms: [ // Add configuration file defaults async (schematic, current) => { - const projectName = - typeof current?.project === 'string' ? current.project : this.getProjectName(); + const projectName = typeof current?.project === 'string' ? current.project : undefined; return { ...(await getSchematicDefaults(schematic.collection.name, schematic.name, projectName)), @@ -407,7 +406,24 @@ export abstract class SchematicsCommandModule return projectName; } - return undefined; + if (workspace.projects.size === 0) { + throw new Error( + 'Cannot determine project for command.\n' + + 'This workspace does not contain any projects. ' + + 'Run "ng generate application" or "ng generate library" to create one.', + ); + } + + throw new Error( + 'Cannot determine project for command.\n' + + 'This is a multi-project workspace and more than one project supports this command. ' + + 'Use the "--project" option to specify a project or change the current ' + + 'working directory to a project directory.\n\n' + + `Available projects are:\n${[...workspace.projects.keys()] + .sort() + .map((p) => `- ${p}`) + .join('\n')}`, + ); } private getResolvePaths(collectionName: string): string[] { diff --git a/tests/e2e/tests/commands/project-cannot-be-determined-by-cwd.ts b/tests/e2e/tests/commands/project-cannot-be-determined-by-cwd.ts index b4b572bfa3ee..f0eaf7633482 100644 --- a/tests/e2e/tests/commands/project-cannot-be-determined-by-cwd.ts +++ b/tests/e2e/tests/commands/project-cannot-be-determined-by-cwd.ts @@ -1,6 +1,6 @@ import assert from 'node:assert/strict'; import { join } from 'node:path'; -import { execAndWaitForOutputToMatch, ng } from '../../utils/process'; +import { execAndCaptureError, execAndWaitForOutputToMatch, ng } from '../../utils/process'; import { updateJsonFile } from '../../utils/project'; import { expectToFail } from '../../utils/utils'; @@ -23,6 +23,29 @@ export default async function () { const { message } = await expectToFail(() => ng('build')); assert.match(message, new RegExp(errorMessage)); + // Schematics that require a project should explain how to select one + const { message: generateMessage } = await expectToFail(() => + ng('generate', 'component', 'foo'), + ); + assert.match(generateMessage, new RegExp(errorMessage)); + assert.match(generateMessage, /Use the "--project" option to specify a project/); + assert.match(generateMessage, /Available projects are:\n- second-app\n- third-app/); + + // The project is checked before prompting for other options + const { message: interactiveMessage } = await execAndCaptureError( + 'ng', + ['generate', 'component'], + { + ...process.env, + NG_FORCE_TTY: '1', + NG_CLI_ANALYTICS: 'false', + NG_FORCE_AUTOCOMPLETE: 'false', + }, + 'foo\n', + ); + assert.match(interactiveMessage, /Use the "--project" option to specify a project/); + assert.doesNotMatch(interactiveMessage, /What name would you like to use for the component/); + // Help should still work await execAndWaitForOutputToMatch('ng', ['build', '--help'], /--configuration/); @@ -31,8 +54,21 @@ export default async function () { process.chdir(join(startCwd, 'projects/second-app')); await ng('build', '--configuration=development'); + await ng('generate', 'component', 'foo', '--dry-run'); } finally { // Restore path process.chdir(startCwd); } + + // A project from the workspace schematic options is used and other validation errors are kept + await updateJsonFile('angular.json', (workspaceJson) => { + workspaceJson.schematics ??= {}; + workspaceJson.schematics['@schematics/angular:component'] = { project: 'second-app' }; + }); + const { message: selectorMessage } = await expectToFail(() => + ng('generate', 'component', 'foo', '--selector=1foo'), + ); + assert.match(selectorMessage, /must match format "html-selector"/); + assert.doesNotMatch(selectorMessage, /Cannot determine project/); + await ng('generate', 'component', 'foo', '--dry-run'); }