From 2cfd33332acc42e1286e24ad69c014f5f3e62a8d Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Mon, 12 Oct 2026 07:51:58 +0000 Subject: [PATCH 1/2] fix(@angular/cli): resolve package name in npm workspace members Read `package.json` in the working directory first in `getCurrentPackageName` to avoid spawning an unnecessary child process, and handle single-entry workspace objects returned by `npm pkg get name` when falling back to the package manager command. Closes #32787 --- .../src/package-managers/package-manager.ts | 34 ++++++--- .../package-managers/package-manager_spec.ts | 74 +++++++++++++++++++ 2 files changed, 98 insertions(+), 10 deletions(-) diff --git a/packages/angular/cli/src/package-managers/package-manager.ts b/packages/angular/cli/src/package-managers/package-manager.ts index 234b54851232..272454c21cbd 100644 --- a/packages/angular/cli/src/package-managers/package-manager.ts +++ b/packages/angular/cli/src/package-managers/package-manager.ts @@ -393,25 +393,39 @@ export class PackageManager { * Gets the name of the package in the current project. */ async getCurrentPackageName(): Promise { + try { + const content = await this.host.readFile(join(this.cwd, 'package.json')); + const { name } = JSON.parse(content) as { name?: unknown }; + if (typeof name === 'string') { + return name; + } + } catch { + // Fall back to package manager command if reading file fails + } + if (this.descriptor.getPackageNameCommand) { try { const { stdout } = await this.#run(this.descriptor.getPackageNameCommand); if (stdout) { - return JSON.parse(stdout); + const name = JSON.parse(stdout); + if (typeof name === 'string') { + return name; + } + + // In a workspace member npm prints an object keyed by workspace name instead of a string. + if (name && typeof name === 'object') { + const values = Object.values(name); + if (values.length === 1 && typeof values[0] === 'string') { + return values[0]; + } + } } } catch { - // Fall back to reading file if command fails + // Ignore command failure } } - try { - const content = await this.host.readFile(join(this.cwd, 'package.json')); - const pkgJson = JSON.parse(content); - - return pkgJson.name; - } catch { - return undefined; - } + return undefined; } /** diff --git a/packages/angular/cli/src/package-managers/package-manager_spec.ts b/packages/angular/cli/src/package-managers/package-manager_spec.ts index d500a435d0df..a1e6b7d9dfda 100644 --- a/packages/angular/cli/src/package-managers/package-manager_spec.ts +++ b/packages/angular/cli/src/package-managers/package-manager_spec.ts @@ -378,6 +378,80 @@ describe('PackageManager', () => { }); }); + describe('getCurrentPackageName', () => { + it('should return the name from package.json without running the package manager command', async () => { + const pm = new PackageManager(host, '/repo/app', descriptor); + const readFileSpy = spyOn(host, 'readFile').and.resolveTo('{"name": "app"}'); + + expect(await pm.getCurrentPackageName()).toBe('app'); + expect(readFileSpy).toHaveBeenCalledWith(jasmine.stringMatching(/package\.json$/)); + expect(runCommandSpy).not.toHaveBeenCalled(); + }); + + it('should fall back to the package manager command when reading package.json fails', async () => { + const pm = new PackageManager(host, '/repo/app/src', descriptor); + spyOn(host, 'readFile').and.rejectWith(new Error('ENOENT')); + runCommandSpy.and.resolveTo({ stdout: '"app"', stderr: '' }); + + expect(await pm.getCurrentPackageName()).toBe('app'); + expect(runCommandSpy).toHaveBeenCalledWith( + descriptor.binary, + descriptor.getPackageNameCommand, + jasmine.objectContaining({ cwd: '/repo/app/src' }), + ); + }); + + it('should extract the package name when npm prints a single-entry workspace object', async () => { + const pm = new PackageManager(host, '/repo/apps/app/src', descriptor); + spyOn(host, 'readFile').and.rejectWith(new Error('ENOENT')); + // Inside a workspace member, `npm pkg get name` prints `{ "app": "app" }` instead of `"app"`. + runCommandSpy.and.resolveTo({ stdout: '{\n "app": "app"\n}', stderr: '' }); + + expect(await pm.getCurrentPackageName()).toBe('app'); + }); + + it('should return undefined when npm prints multiple workspace entries and package.json is unavailable', async () => { + const pm = new PackageManager(host, '/repo', descriptor); + spyOn(host, 'readFile').and.rejectWith(new Error('ENOENT')); + runCommandSpy.and.resolveTo({ + stdout: '{\n "app-a": "app-a",\n "app-b": "app-b"\n}', + stderr: '', + }); + + expect(await pm.getCurrentPackageName()).toBeUndefined(); + }); + }); + + describe('getProjectDependencies', () => { + it('should list the dependencies of an npm workspace member', async () => { + const pm = new PackageManager(host, '/repo/apps/app', descriptor); + runCommandSpy.and.callFake((_binary: string, args: readonly string[]) => + Promise.resolve({ + stdout: + args[0] === 'pkg' + ? '{\n "app": "app"\n}' + : JSON.stringify({ + name: 'monorepo', + dependencies: { + app: { + version: '0.0.0', + resolved: 'file:../apps/app', + dependencies: { '@angular/core': { version: '22.0.0' } }, + }, + }, + }), + stderr: '', + }), + ); + spyOn(host, 'readFile').and.resolveTo('{"name": "app"}'); + + const dependencies = await pm.getProjectDependencies(); + + expect([...dependencies.keys()]).toEqual(['@angular/core']); + expect(dependencies.get('@angular/core')?.version).toBe('22.0.0'); + }); + }); + describe('initializationError', () => { it('should throw initializationError when running commands', async () => { const error = new Error('Not installed'); From 088f1967d034d1a28f5634b7d630b6469447f22c Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Mon, 12 Oct 2026 10:29:31 +0000 Subject: [PATCH 2/2] fixup! fix(@angular/cli): resolve package name in npm workspace members --- .../package-managers/package-manager_spec.ts | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/packages/angular/cli/src/package-managers/package-manager_spec.ts b/packages/angular/cli/src/package-managers/package-manager_spec.ts index a1e6b7d9dfda..37e0d8aae113 100644 --- a/packages/angular/cli/src/package-managers/package-manager_spec.ts +++ b/packages/angular/cli/src/package-managers/package-manager_spec.ts @@ -425,24 +425,19 @@ describe('PackageManager', () => { describe('getProjectDependencies', () => { it('should list the dependencies of an npm workspace member', async () => { const pm = new PackageManager(host, '/repo/apps/app', descriptor); - runCommandSpy.and.callFake((_binary: string, args: readonly string[]) => - Promise.resolve({ - stdout: - args[0] === 'pkg' - ? '{\n "app": "app"\n}' - : JSON.stringify({ - name: 'monorepo', - dependencies: { - app: { - version: '0.0.0', - resolved: 'file:../apps/app', - dependencies: { '@angular/core': { version: '22.0.0' } }, - }, - }, - }), - stderr: '', + runCommandSpy.and.resolveTo({ + stdout: JSON.stringify({ + name: 'monorepo', + dependencies: { + app: { + version: '0.0.0', + resolved: 'file:../apps/app', + dependencies: { '@angular/core': { version: '22.0.0' } }, + }, + }, }), - ); + stderr: '', + }); spyOn(host, 'readFile').and.resolveTo('{"name": "app"}'); const dependencies = await pm.getProjectDependencies();