Skip to content

Commit f5195bf

Browse files
committed
fix(@angular/cli): support resolving subproject dependencies in pnpm workspaces
When running dependency listing commands (like `pnpm list --depth=0 --json`) in a workspace subproject, the package manager resolves the workspace layout and runs in the context of the workspace root, returning an array of project objects representing all workspace members. Previously, `parseNpmLikeDependencies` blindly selected the first item in the array (`data[0]`), which corresponds to the workspace root package. Since the root package typically does not declare workspace member dependencies (like `@angular/core`), `ng update` failed with "Package X is not a dependency" errors. This updates `parseNpmLikeDependencies` to search the array for the project matching `workspacePackageName` if it is provided. If the selected project is the workspace member itself, it bypasses the nested workspace dependency lookup block, allowing its direct dependencies to be parsed correctly.
1 parent 035c72a commit f5195bf

2 files changed

Lines changed: 134 additions & 19 deletions

File tree

packages/angular/cli/src/package-managers/parsers.ts

Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,13 @@ interface NpmListDependency {
7979
* @param logger An optional logger instance.
8080
* @returns A map of package names to their installed package details.
8181
*/
82+
interface NpmListProject {
83+
name?: string;
84+
dependencies?: Record<string, NpmListDependency>;
85+
devDependencies?: Record<string, NpmListDependency>;
86+
unsavedDependencies?: Record<string, NpmListDependency>;
87+
}
88+
8289
export function parseNpmLikeDependencies(
8390
stdout: string,
8491
logger?: Logger,
@@ -94,26 +101,55 @@ export function parseNpmLikeDependencies(
94101
return dependencies;
95102
}
96103

97-
let data = JSON.parse(stdout);
98-
if (Array.isArray(data)) {
99-
// pnpm returns an array of projects.
100-
data = data[0];
101-
}
104+
const data = JSON.parse(stdout);
105+
const workspacePackageName = options?.workspacePackageName;
102106

103-
const dependencyMaps = [data.dependencies, data.devDependencies, data.unsavedDependencies].filter(
104-
(d) => !!d,
105-
);
107+
let rootProject: NpmListProject | undefined;
108+
let subProject: NpmListProject | undefined;
106109

107-
if (dependencyMaps.length === 0) {
108-
logger?.debug(' `dependencies` property not found. No dependencies found.');
110+
if (Array.isArray(data)) {
111+
rootProject = data[0];
112+
if (workspacePackageName) {
113+
subProject = data.find(
114+
(project) => (project as NpmListProject | undefined)?.name === workspacePackageName,
115+
);
116+
}
117+
} else {
118+
rootProject = data;
119+
}
109120

121+
if (!rootProject || typeof rootProject !== 'object') {
110122
return dependencies;
111123
}
112124

113-
const workspacePackageName = options?.workspacePackageName;
114-
115-
if (workspacePackageName) {
116-
for (const dependencyMap of dependencyMaps) {
125+
// 1. Extract subproject dependencies first to prioritize them
126+
if (subProject) {
127+
const subprojectMaps = [
128+
subProject.dependencies,
129+
subProject.devDependencies,
130+
subProject.unsavedDependencies,
131+
].filter((d) => !!d) as Record<string, NpmListDependency>[];
132+
133+
for (const subprojectMap of subprojectMaps) {
134+
for (const [name, info] of Object.entries(subprojectMap)) {
135+
if (info && typeof info === 'object' && info.version) {
136+
dependencies.set(name, {
137+
name,
138+
version: info.version,
139+
path: info.path,
140+
});
141+
}
142+
}
143+
}
144+
} else if (workspacePackageName) {
145+
// Fallback to npm-like nested lookup inside the root project
146+
const rootDependencyMaps = [
147+
rootProject.dependencies,
148+
rootProject.devDependencies,
149+
rootProject.unsavedDependencies,
150+
].filter((d) => !!d) as Record<string, NpmListDependency>[];
151+
152+
for (const dependencyMap of rootDependencyMaps) {
117153
const info = dependencyMap[workspacePackageName];
118154
if (info && typeof info === 'object') {
119155
const nestedMaps = [
@@ -137,11 +173,15 @@ export function parseNpmLikeDependencies(
137173
}
138174
}
139175

140-
// Extract top-level dependencies (root), without overwriting subproject dependencies
141-
for (const dependencyMap of dependencyMaps) {
142-
for (const [name, info] of Object.entries(
143-
dependencyMap as Record<string, NpmListDependency & { resolved?: string }>,
144-
)) {
176+
// 2. Supplement with hoisted/root dependencies (without overwriting subproject dependencies)
177+
const rootDependencyMaps = [
178+
rootProject.dependencies,
179+
rootProject.devDependencies,
180+
rootProject.unsavedDependencies,
181+
].filter((d) => !!d) as Record<string, NpmListDependency & { resolved?: string }>[];
182+
183+
for (const dependencyMap of rootDependencyMaps) {
184+
for (const [name, info] of Object.entries(dependencyMap)) {
145185
if (!info || typeof info !== 'object') {
146186
continue;
147187
}

packages/angular/cli/src/package-managers/parsers_spec.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,81 @@ describe('parsers', () => {
6464
path: undefined,
6565
});
6666
});
67+
68+
it('should parse dependencies from array when workspacePackageName matches a project', () => {
69+
const stdout = JSON.stringify([
70+
{
71+
name: 'monorepo-root',
72+
version: '1.0.0',
73+
path: '/workspace',
74+
dependencies: {
75+
typescript: {
76+
version: '5.9.3',
77+
},
78+
},
79+
},
80+
{
81+
name: 'app',
82+
version: '1.0.0',
83+
path: '/workspace/apps/test-app',
84+
dependencies: {
85+
rxjs: {
86+
version: '7.8.2',
87+
},
88+
},
89+
},
90+
]);
91+
92+
const deps = parseNpmLikeDependencies(stdout, undefined, { workspacePackageName: 'app' });
93+
expect(deps.size).toBe(2);
94+
expect(deps.get('rxjs')).toEqual({ name: 'rxjs', version: '7.8.2', path: undefined });
95+
expect(deps.get('typescript')).toEqual({
96+
name: 'typescript',
97+
version: '5.9.3',
98+
path: undefined,
99+
});
100+
});
101+
102+
it('should parse dependencies from array when workspacePackageName matches the root project', () => {
103+
const stdout = JSON.stringify([
104+
{
105+
name: 'monorepo-root',
106+
version: '1.0.0',
107+
path: '/workspace',
108+
dependencies: {
109+
typescript: {
110+
version: '5.9.3',
111+
},
112+
},
113+
},
114+
{
115+
name: 'other-app',
116+
version: '1.0.0',
117+
path: '/workspace/apps/other-app',
118+
dependencies: {
119+
rxjs: {
120+
version: '7.8.2',
121+
},
122+
},
123+
},
124+
]);
125+
126+
const deps = parseNpmLikeDependencies(stdout, undefined, {
127+
workspacePackageName: 'monorepo-root',
128+
});
129+
expect(deps.size).toBe(1);
130+
expect(deps.get('typescript')).toEqual({
131+
name: 'typescript',
132+
version: '5.9.3',
133+
path: undefined,
134+
});
135+
});
136+
137+
it('should return empty map and not throw on empty array or non-object json', () => {
138+
expect(parseNpmLikeDependencies('[]').size).toBe(0);
139+
expect(parseNpmLikeDependencies('null').size).toBe(0);
140+
expect(parseNpmLikeDependencies('"not-an-object"').size).toBe(0);
141+
});
67142
});
68143

69144
describe('parseNpmLikeError', () => {

0 commit comments

Comments
 (0)