diff --git a/packages/angular/cli/src/package-managers/package-manager-descriptor.ts b/packages/angular/cli/src/package-managers/package-manager-descriptor.ts index 34db06b64c99..2dfe75ee01cd 100644 --- a/packages/angular/cli/src/package-managers/package-manager-descriptor.ts +++ b/packages/angular/cli/src/package-managers/package-manager-descriptor.ts @@ -87,6 +87,9 @@ export interface PackageManagerDescriptor { /** The command to list all installed dependencies. */ readonly listDependenciesCommand: readonly string[]; + /** The command to get the current package name. */ + readonly getPackageNameCommand?: readonly string[]; + /** The command to fetch the registry manifest of a package. */ readonly getManifestCommand: readonly string[]; @@ -99,7 +102,11 @@ export interface PackageManagerDescriptor { /** A collection of functions to parse the output of specific commands. */ readonly outputParsers: { /** A function to parse the output of `listDependenciesCommand`. */ - listDependencies: (stdout: string, logger?: Logger) => Map; + listDependencies: ( + stdout: string, + logger?: Logger, + options?: { workspacePackageName?: string }, + ) => Map; /** A function to parse the output of `getManifestCommand` for a specific version. */ getRegistryManifest: (stdout: string, logger?: Logger) => PackageManifest | null; @@ -158,6 +165,7 @@ export const SUPPORTED_PACKAGE_MANAGERS = { getRegistryOptions: (registry: string) => ({ args: ['--registry', registry] }), versionCommand: ['--version'], listDependenciesCommand: ['list', '--depth=0', '--json=true', '--all=true'], + getPackageNameCommand: ['pkg', 'get', 'name'], getManifestCommand: ['view', '--json'], viewCommandFieldArgFormatter: (fields) => [...fields], outputParsers: { @@ -237,6 +245,7 @@ export const SUPPORTED_PACKAGE_MANAGERS = { getRegistryOptions: (registry: string) => ({ args: ['--registry', registry] }), versionCommand: ['--version'], listDependenciesCommand: ['list', '--depth=0', '--json'], + getPackageNameCommand: ['pkg', 'get', 'name'], getManifestCommand: ['view', '--json'], viewCommandFieldArgFormatter: (fields) => [...fields], outputParsers: { diff --git a/packages/angular/cli/src/package-managers/package-manager.ts b/packages/angular/cli/src/package-managers/package-manager.ts index 0dfb89e57371..33b8b07d48e3 100644 --- a/packages/angular/cli/src/package-managers/package-manager.ts +++ b/packages/angular/cli/src/package-managers/package-manager.ts @@ -145,8 +145,9 @@ export class PackageManager { const args = this.descriptor.listDependenciesCommand; + const workspacePackageName = await this.getCurrentPackageName(); const dependencies = await this.#fetchAndParse(args, (stdout, logger) => - this.descriptor.outputParsers.listDependencies(stdout, logger), + this.descriptor.outputParsers.listDependencies(stdout, logger, { workspacePackageName }), ); return (this.#dependencyCache = dependencies ?? new Map()); @@ -361,6 +362,31 @@ export class PackageManager { this.#dependencyCache = null; } + /** + * Gets the name of the package in the current project. + */ + async getCurrentPackageName(): Promise { + if (this.descriptor.getPackageNameCommand) { + try { + const { stdout } = await this.#run(this.descriptor.getPackageNameCommand); + if (stdout) { + return JSON.parse(stdout); + } + } catch { + // Fall back to reading file if command fails + } + } + + try { + const content = await this.host.readFile(join(this.cwd, 'package.json')); + const pkgJson = JSON.parse(content); + + return pkgJson.name; + } catch { + return undefined; + } + } + /** * Gets the version of the package manager binary. */ diff --git a/packages/angular/cli/src/package-managers/parsers.ts b/packages/angular/cli/src/package-managers/parsers.ts index c9f7fb235087..0fe498c12331 100644 --- a/packages/angular/cli/src/package-managers/parsers.ts +++ b/packages/angular/cli/src/package-managers/parsers.ts @@ -81,6 +81,7 @@ interface NpmListDependency { export function parseNpmLikeDependencies( stdout: string, logger?: Logger, + options?: { workspacePackageName?: string }, ): Map { logger?.debug(`Parsing npm-like dependency list...`); logStdout(stdout, logger); @@ -108,13 +109,56 @@ export function parseNpmLikeDependencies( return dependencies; } + const workspacePackageName = options?.workspacePackageName; + + if (workspacePackageName) { + for (const dependencyMap of dependencyMaps) { + const info = dependencyMap[workspacePackageName]; + if (info && typeof info === 'object') { + const nestedMaps = [ + info.dependencies, + info.devDependencies, + info.unsavedDependencies, + ].filter((d) => !!d) as Record[]; + + for (const nestedMap of nestedMaps) { + for (const [name, nestedInfo] of Object.entries(nestedMap)) { + if (nestedInfo && typeof nestedInfo === 'object' && nestedInfo.version) { + dependencies.set(name, { + name, + version: nestedInfo.version, + path: nestedInfo.path, + }); + } + } + } + } + } + } + + // Extract top-level dependencies (root), without overwriting subproject 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, - }); + for (const [name, info] of Object.entries( + dependencyMap as Record, + )) { + if (!info || typeof info !== 'object') { + continue; + } + + // Exclude local monorepo workspace packages (which originate from a local file/dir + // and contain nested dependency maps in the output of `npm list --depth=0`), + // while preserving third-party packages installed from local paths. + const isWorkspacePackage = + info.resolved?.startsWith('file:') && + (!!info.dependencies || !!info.devDependencies || !!info.unsavedDependencies); + + if (info.version && !dependencies.has(name) && !isWorkspacePackage) { + dependencies.set(name, { + name, + version: info.version, + path: info.path, + }); + } } } diff --git a/packages/angular/cli/src/package-managers/parsers_spec.ts b/packages/angular/cli/src/package-managers/parsers_spec.ts index 2fa8abdc1e32..6d21300c7009 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,51 @@ 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 dependencies from current subproject and hoisted root', () => { + const stdout = JSON.stringify({ + version: '1.0.0', + name: 'monorepo-root', + dependencies: { + app: { + version: '1.0.0', + resolved: 'file:../packages/app', + dependencies: { + rxjs: { + version: '7.8.1', + }, + }, + }, + typescript: { + version: '5.9.3', + }, + }, + }); + + const deps = parseNpmLikeDependencies(stdout, undefined, { workspacePackageName: 'app' }); + expect(deps.size).toBe(2); + expect(deps.get('rxjs')).toEqual({ name: 'rxjs', version: '7.8.1', path: undefined }); + expect(deps.get('typescript')).toEqual({ + name: 'typescript', + version: '5.9.3', + path: undefined, + }); + }); + }); + describe('parseNpmLikeError', () => { it('should parse a structured JSON error from modern yarn', () => { const stdout = JSON.stringify({