Skip to content

Commit 8101da3

Browse files
committed
fix(@angular/cli): resolve schematic working directory from project root
Compute the workingDirectory smart default relative to the active project root so ng generate from a multi-project application directory uses src/app defaults instead of the project folder itself. Fixes #21660
1 parent bf1602a commit 8101da3

3 files changed

Lines changed: 100 additions & 6 deletions

File tree

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

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ import { relative } from 'node:path';
1717
import { Argv } from 'yargs';
1818
import { isPackageNameSafeForAnalytics } from '../analytics/analytics';
1919
import { EventCustomDimension } from '../analytics/analytics-parameters';
20-
import { getProjectByCwd, getSchematicDefaults } from '../utilities/config';
20+
import {
21+
getProjectByCwd,
22+
getSchematicDefaults,
23+
getSchematicWorkingDirectory,
24+
} from '../utilities/config';
2125
import { assertIsError } from '../utilities/error';
2226
import { formatFiles } from '../utilities/formatting';
2327
import { memoize } from '../utilities/memoize';
@@ -138,10 +142,14 @@ export abstract class SchematicsCommandModule
138142
workflow.registry.useXDeprecatedProvider((msg) => logger.warn(msg));
139143
workflow.registry.addSmartDefaultProvider('projectName', () => this.getProjectName());
140144

141-
const workingDir = devkitNormalize(relative(this.context.root, process.cwd()));
142-
workflow.registry.addSmartDefaultProvider('workingDirectory', () =>
143-
workingDir === '' ? undefined : workingDir,
144-
);
145+
const workingDirectory = this.context.workspace
146+
? getSchematicWorkingDirectory(this.context.workspace)
147+
: (() => {
148+
const dir = devkitNormalize(relative(root, process.cwd()));
149+
150+
return dir === '' ? undefined : dir;
151+
})();
152+
workflow.registry.addSmartDefaultProvider('workingDirectory', () => workingDirectory);
145153

146154
workflow.engineHost.registerOptionsTransform(async (schematic, options) => {
147155
const {

‎packages/angular/cli/src/utilities/config.ts‎

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import { json, workspaces } from '@angular-devkit/core';
9+
import { json, normalize as devkitNormalize, workspaces } from '@angular-devkit/core';
1010
import { existsSync, promises as fs } from 'node:fs';
1111
import * as os from 'node:os';
1212
import * as path from 'node:path';
@@ -323,6 +323,35 @@ export function getProjectByCwd(workspace: AngularWorkspace): string | null {
323323
return null;
324324
}
325325

326+
/**
327+
* Resolves the schematic `workingDirectory` smart default relative to the active project root.
328+
* When the cwd is the project root, `undefined` is returned so schematics can apply their own
329+
* default path (e.g. `src/app`).
330+
*/
331+
export function getSchematicWorkingDirectory(
332+
workspace: AngularWorkspace,
333+
cwd: string = process.cwd(),
334+
): string | undefined {
335+
const projectName =
336+
workspace.projects.size === 1
337+
? Array.from(workspace.projects.keys())[0]
338+
: findProjectByPath(workspace, cwd);
339+
340+
if (projectName) {
341+
const project = workspace.projects.get(projectName);
342+
if (project) {
343+
const projectRoot = path.join(workspace.basePath, project.root);
344+
const projectRelative = devkitNormalize(path.relative(projectRoot, cwd));
345+
346+
return projectRelative === '' ? undefined : projectRelative;
347+
}
348+
}
349+
350+
const workspaceRelative = devkitNormalize(path.relative(workspace.basePath, cwd));
351+
352+
return workspaceRelative === '' ? undefined : workspaceRelative;
353+
}
354+
326355
export async function getConfiguredPackageManager(): Promise<PackageManager | null> {
327356
const getPackageManager = (source: json.JsonValue | undefined): PackageManager | null => {
328357
if (isJsonObject(source)) {
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { workspaces } from '@angular-devkit/core';
10+
import { join, resolve } from 'node:path';
11+
import { AngularWorkspace, getSchematicWorkingDirectory } from './config';
12+
13+
describe('getSchematicWorkingDirectory', () => {
14+
const workspaceRoot = resolve('/tmp/workspace');
15+
16+
function mockWorkspace(
17+
projects: Record<string, { root: string }>,
18+
): AngularWorkspace {
19+
const collection = new workspaces.ProjectDefinitionCollection();
20+
for (const [name, { root }] of Object.entries(projects)) {
21+
collection.set(name, { root } as workspaces.ProjectDefinition);
22+
}
23+
24+
return {
25+
basePath: workspaceRoot,
26+
projects: collection,
27+
} as unknown as AngularWorkspace;
28+
}
29+
30+
it('returns undefined when cwd is a multi-project application root', () => {
31+
const workspace = mockWorkspace({
32+
app1: { root: 'projects/app1' },
33+
app2: { root: 'projects/app2' },
34+
});
35+
const cwd = join(workspaceRoot, 'projects/app1');
36+
37+
expect(getSchematicWorkingDirectory(workspace, cwd)).toBeUndefined();
38+
});
39+
40+
it('returns a path relative to the project when cwd is inside the project tree', () => {
41+
const workspace = mockWorkspace({
42+
app1: { root: 'projects/app1' },
43+
app2: { root: 'projects/app2' },
44+
});
45+
const cwd = join(workspaceRoot, 'projects/app1/src/app/features');
46+
47+
expect(getSchematicWorkingDirectory(workspace, cwd)).toBe('src/app/features');
48+
});
49+
50+
it('returns undefined for a single-project workspace when cwd is the workspace root', () => {
51+
const workspace = mockWorkspace({
52+
app: { root: '' },
53+
});
54+
55+
expect(getSchematicWorkingDirectory(workspace, workspaceRoot)).toBeUndefined();
56+
});
57+
});

0 commit comments

Comments
 (0)