Repository navigation
fix(@angular/cli): resolve schematic working directory from project root #34608
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,7 +6,7 @@ | |
| * found in the LICENSE file at https://angular.dev/license | ||
| */ | ||
|
|
||
| import { json, workspaces } from '@angular-devkit/core'; | ||
| import { json, normalize as devkitNormalize, workspaces } from '@angular-devkit/core'; | ||
| import { existsSync, promises as fs } from 'node:fs'; | ||
| import * as os from 'node:os'; | ||
| import * as path from 'node:path'; | ||
|
|
@@ -323,6 +323,35 @@ export function getProjectByCwd(workspace: AngularWorkspace): string | null { | |
| return null; | ||
| } | ||
|
|
||
| /** | ||
| * Resolves the schematic `workingDirectory` smart default relative to the active project root. | ||
| * When the cwd is the project root, `undefined` is returned so schematics can apply their own | ||
| * default path (e.g. `src/app`). | ||
| */ | ||
| export function getSchematicWorkingDirectory( | ||
| workspace: AngularWorkspace, | ||
| cwd: string = process.cwd(), | ||
| ): string | undefined { | ||
| const projectName = | ||
| workspace.projects.size === 1 | ||
| ? Array.from(workspace.projects.keys())[0] | ||
| : findProjectByPath(workspace, cwd); | ||
|
|
||
| if (projectName) { | ||
| const project = workspace.projects.get(projectName); | ||
| if (project) { | ||
| const projectRoot = path.join(workspace.basePath, project.root); | ||
| const projectRelative = devkitNormalize(path.relative(projectRoot, cwd)); | ||
|
|
||
| return projectRelative === '' ? undefined : projectRelative; | ||
| } | ||
| } | ||
|
|
||
| const workspaceRelative = devkitNormalize(path.relative(workspace.basePath, cwd)); | ||
|
|
||
| return workspaceRelative === '' ? undefined : workspaceRelative; | ||
|
Comment on lines
+350
to
+352
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similarly, if const workspaceRelative = devkitNormalize(path.relative(workspace.basePath, cwd));
if (workspaceRelative.startsWith('..') || path.isAbsolute(workspaceRelative)) {
return undefined;
}
return workspaceRelative === '' ? undefined : workspaceRelative; |
||
| } | ||
|
|
||
| export async function getConfiguredPackageManager(): Promise<PackageManager | null> { | ||
| const getPackageManager = (source: json.JsonValue | undefined): PackageManager | null => { | ||
| if (isJsonObject(source)) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| /** | ||
| * @license | ||
| * Copyright Google LLC All Rights Reserved. | ||
| * | ||
| * Use of this source code is governed by an MIT-style license that can be | ||
| * found in the LICENSE file at https://angular.dev/license | ||
| */ | ||
|
|
||
| import { workspaces } from '@angular-devkit/core'; | ||
| import { join, resolve } from 'node:path'; | ||
| import { AngularWorkspace, getSchematicWorkingDirectory } from './config'; | ||
|
|
||
| describe('getSchematicWorkingDirectory', () => { | ||
| const workspaceRoot = resolve('/tmp/workspace'); | ||
|
|
||
| function mockWorkspace( | ||
| projects: Record<string, { root: string }>, | ||
| ): AngularWorkspace { | ||
| const collection = new workspaces.ProjectDefinitionCollection(); | ||
| for (const [name, { root }] of Object.entries(projects)) { | ||
| collection.set(name, { root } as workspaces.ProjectDefinition); | ||
| } | ||
|
|
||
| return { | ||
| basePath: workspaceRoot, | ||
| projects: collection, | ||
| } as unknown as AngularWorkspace; | ||
| } | ||
|
|
||
| it('returns undefined when cwd is a multi-project application root', () => { | ||
| const workspace = mockWorkspace({ | ||
| app1: { root: 'projects/app1' }, | ||
| app2: { root: 'projects/app2' }, | ||
| }); | ||
| const cwd = join(workspaceRoot, 'projects/app1'); | ||
|
|
||
| expect(getSchematicWorkingDirectory(workspace, cwd)).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('returns a path relative to the project when cwd is inside the project tree', () => { | ||
| const workspace = mockWorkspace({ | ||
| app1: { root: 'projects/app1' }, | ||
| app2: { root: 'projects/app2' }, | ||
| }); | ||
| const cwd = join(workspaceRoot, 'projects/app1/src/app/features'); | ||
|
|
||
| expect(getSchematicWorkingDirectory(workspace, cwd)).toBe('src/app/features'); | ||
| }); | ||
|
|
||
| it('returns undefined for a single-project workspace when cwd is the workspace root', () => { | ||
| const workspace = mockWorkspace({ | ||
| app: { root: '' }, | ||
| }); | ||
|
|
||
| expect(getSchematicWorkingDirectory(workspace, workspaceRoot)).toBeUndefined(); | ||
| }); | ||
|
Comment on lines
+50
to
+56
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a unit test to verify that it('returns undefined for a single-project workspace when cwd is the workspace root', () => {
const workspace = mockWorkspace({
app: { root: '' },
});
expect(getSchematicWorkingDirectory(workspace, workspaceRoot)).toBeUndefined();
});
it('returns undefined for a single-project workspace when cwd is outside the project root', () => {
const workspace = mockWorkspace({
app: { root: 'projects/app' },
});
expect(getSchematicWorkingDirectory(workspace, workspaceRoot)).toBeUndefined();
}); |
||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In a single-project workspace where the project root is not the workspace root (e.g.,
projects/app), if the command is run from the workspace root,workspace.projects.size === 1will eagerly select the single project. This causespath.relative(projectRoot, cwd)to resolve to a path escaping the project root (e.g.,../..). Returning this relative path will cause schematics to generate files outside the project root (at the workspace root).To prevent this, we should check if the resolved relative path starts with
..or is absolute, and returnundefinedin those cases so that schematics fall back to their default paths.