Skip to content

Commit 8532c3b

Browse files
committed
fix(@angular/cli): recursively collect nested workspace dependencies in npm
When running the update command in an npm workspace repository from within a workspace subdirectory, the CLI currently fails to detect hoisted dependencies. This occurs because npm list structures its workspace dependency output as nested items inside their respective top-level workspace entry, rather than as top-level items. The current parser was only reading the top-level items and consequently missed nested workspace dependencies. This change updates the dependency parser to perform a breadth-first traversal of the JSON tree output of the package list command. By iteratively traversing the nested dependencies, the CLI can successfully resolve all installed packages within an npm workspaces monorepo.
1 parent 73233dc commit 8532c3b

2 files changed

Lines changed: 64 additions & 7 deletions

File tree

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

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -108,13 +108,32 @@ export function parseNpmLikeDependencies(
108108
return dependencies;
109109
}
110110

111-
for (const dependencyMap of dependencyMaps) {
112-
for (const [name, info] of Object.entries(dependencyMap as Record<string, NpmListDependency>)) {
113-
dependencies.set(name, {
114-
name,
115-
version: info.version,
116-
path: info.path,
117-
});
111+
// Perform a breadth-first traversal to collect dependencies.
112+
// The queue size is bounded because `npm list` is executed with `--depth=0`,
113+
// which limits the traversal to top-level dependencies of the workspaces.
114+
const queue = [...dependencyMaps];
115+
let index = 0;
116+
while (index < queue.length) {
117+
const currentMap = queue[index++] as Record<string, NpmListDependency> | undefined;
118+
if (!currentMap) {
119+
continue;
120+
}
121+
for (const [name, info] of Object.entries(currentMap)) {
122+
if (info && typeof info === 'object') {
123+
if (info.version && !dependencies.has(name)) {
124+
dependencies.set(name, {
125+
name,
126+
version: info.version,
127+
path: info.path,
128+
});
129+
}
130+
const nestedMaps = [
131+
info.dependencies,
132+
info.devDependencies,
133+
info.unsavedDependencies,
134+
].filter((d) => !!d);
135+
queue.push(...nestedMaps);
136+
}
118137
}
119138
}
120139

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import {
1010
parseBunDependencies,
11+
parseNpmLikeDependencies,
1112
parseNpmLikeError,
1213
parseNpmLikeManifest,
1314
parseYarnClassicDependencies,
@@ -16,6 +17,43 @@ import {
1617
} from './parsers';
1718

1819
describe('parsers', () => {
20+
describe('parseNpmLikeDependencies', () => {
21+
it('should parse simple dependencies', () => {
22+
const stdout = JSON.stringify({
23+
dependencies: {
24+
rxjs: {
25+
version: '7.8.2',
26+
},
27+
},
28+
});
29+
const deps = parseNpmLikeDependencies(stdout);
30+
expect(deps.size).toBe(1);
31+
expect(deps.get('rxjs')).toEqual({ name: 'rxjs', version: '7.8.2', path: undefined });
32+
});
33+
34+
it('should parse nested workspace dependencies for npm workspaces', () => {
35+
const stdout = JSON.stringify({
36+
version: '1.0.0',
37+
name: 'npm-workspace-test',
38+
dependencies: {
39+
app: {
40+
version: '1.0.0',
41+
resolved: 'file:../packages/app',
42+
dependencies: {
43+
rxjs: {
44+
version: '7.8.1',
45+
},
46+
},
47+
},
48+
},
49+
});
50+
const deps = parseNpmLikeDependencies(stdout);
51+
expect(deps.size).toBe(2);
52+
expect(deps.get('app')).toEqual({ name: 'app', version: '1.0.0', path: undefined });
53+
expect(deps.get('rxjs')).toEqual({ name: 'rxjs', version: '7.8.1', path: undefined });
54+
});
55+
});
56+
1957
describe('parseNpmLikeError', () => {
2058
it('should parse a structured JSON error from modern yarn', () => {
2159
const stdout = JSON.stringify({

0 commit comments

Comments
 (0)