diff --git a/packages/angular/cli/src/package-managers/parsers.ts b/packages/angular/cli/src/package-managers/parsers.ts index c9f7fb235087..509e7d16e8fe 100644 --- a/packages/angular/cli/src/package-managers/parsers.ts +++ b/packages/angular/cli/src/package-managers/parsers.ts @@ -108,13 +108,32 @@ export function parseNpmLikeDependencies( return dependencies; } - for (const dependencyMap of dependencyMaps) { - for (const [name, info] of Object.entries(dependencyMap as Record)) { - dependencies.set(name, { - name, - version: info.version, - path: info.path, - }); + // Perform a breadth-first traversal to collect dependencies. + // The queue size is bounded because `npm list` is executed with `--depth=0`, + // which limits the traversal to top-level dependencies of the workspaces. + const queue = [...dependencyMaps]; + let index = 0; + while (index < queue.length) { + const currentMap = queue[index++] as Record | undefined; + if (!currentMap) { + continue; + } + for (const [name, info] of Object.entries(currentMap)) { + if (info && typeof info === 'object') { + if (info.version && !dependencies.has(name)) { + dependencies.set(name, { + name, + version: info.version, + path: info.path, + }); + } + const nestedMaps = [ + info.dependencies, + info.devDependencies, + info.unsavedDependencies, + ].filter((d) => !!d); + queue.push(...nestedMaps); + } } } diff --git a/packages/angular/cli/src/package-managers/parsers_spec.ts b/packages/angular/cli/src/package-managers/parsers_spec.ts index 2fa8abdc1e32..802402f76f5d 100644 --- a/packages/angular/cli/src/package-managers/parsers_spec.ts +++ b/packages/angular/cli/src/package-managers/parsers_spec.ts @@ -8,6 +8,7 @@ import { parseBunDependencies, + parseNpmLikeDependencies, parseNpmLikeError, parseNpmLikeManifest, parseYarnClassicDependencies, @@ -16,6 +17,43 @@ import { } from './parsers'; describe('parsers', () => { + describe('parseNpmLikeDependencies', () => { + it('should parse simple dependencies', () => { + const stdout = JSON.stringify({ + dependencies: { + rxjs: { + version: '7.8.2', + }, + }, + }); + const deps = parseNpmLikeDependencies(stdout); + expect(deps.size).toBe(1); + expect(deps.get('rxjs')).toEqual({ name: 'rxjs', version: '7.8.2', path: undefined }); + }); + + it('should parse nested workspace dependencies for npm workspaces', () => { + const stdout = JSON.stringify({ + version: '1.0.0', + name: 'npm-workspace-test', + dependencies: { + app: { + version: '1.0.0', + resolved: 'file:../packages/app', + dependencies: { + rxjs: { + version: '7.8.1', + }, + }, + }, + }, + }); + const deps = parseNpmLikeDependencies(stdout); + expect(deps.size).toBe(2); + expect(deps.get('app')).toEqual({ name: 'app', version: '1.0.0', path: undefined }); + expect(deps.get('rxjs')).toEqual({ name: 'rxjs', version: '7.8.1', path: undefined }); + }); + }); + describe('parseNpmLikeError', () => { it('should parse a structured JSON error from modern yarn', () => { const stdout = JSON.stringify({