Skip to content

Commit a1cf33b

Browse files
authored
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.
1 parent 39d230e commit a1cf33b

2 files changed

Lines changed: 93 additions & 10 deletions

File tree

‎packages/angular/cli/src/package-managers/package-manager.ts‎

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -393,25 +393,39 @@ export class PackageManager {
393393
* Gets the name of the package in the current project.
394394
*/
395395
async getCurrentPackageName(): Promise<string | undefined> {
396+
try {
397+
const content = await this.host.readFile(join(this.cwd, 'package.json'));
398+
const { name } = JSON.parse(content) as { name?: unknown };
399+
if (typeof name === 'string') {
400+
return name;
401+
}
402+
} catch {
403+
// Fall back to package manager command if reading file fails
404+
}
405+
396406
if (this.descriptor.getPackageNameCommand) {
397407
try {
398408
const { stdout } = await this.#run(this.descriptor.getPackageNameCommand);
399409
if (stdout) {
400-
return JSON.parse(stdout);
410+
const name = JSON.parse(stdout);
411+
if (typeof name === 'string') {
412+
return name;
413+
}
414+
415+
// In a workspace member npm prints an object keyed by workspace name instead of a string.
416+
if (name && typeof name === 'object') {
417+
const values = Object.values(name);
418+
if (values.length === 1 && typeof values[0] === 'string') {
419+
return values[0];
420+
}
421+
}
401422
}
402423
} catch {
403-
// Fall back to reading file if command fails
424+
// Ignore command failure
404425
}
405426
}
406427

407-
try {
408-
const content = await this.host.readFile(join(this.cwd, 'package.json'));
409-
const pkgJson = JSON.parse(content);
410-
411-
return pkgJson.name;
412-
} catch {
413-
return undefined;
414-
}
428+
return undefined;
415429
}
416430

417431
/**

‎packages/angular/cli/src/package-managers/package-manager_spec.ts‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,75 @@ describe('PackageManager', () => {
378378
});
379379
});
380380

381+
describe('getCurrentPackageName', () => {
382+
it('should return the name from package.json without running the package manager command', async () => {
383+
const pm = new PackageManager(host, '/repo/app', descriptor);
384+
const readFileSpy = spyOn(host, 'readFile').and.resolveTo('{"name": "app"}');
385+
386+
expect(await pm.getCurrentPackageName()).toBe('app');
387+
expect(readFileSpy).toHaveBeenCalledWith(jasmine.stringMatching(/package\.json$/));
388+
expect(runCommandSpy).not.toHaveBeenCalled();
389+
});
390+
391+
it('should fall back to the package manager command when reading package.json fails', async () => {
392+
const pm = new PackageManager(host, '/repo/app/src', descriptor);
393+
spyOn(host, 'readFile').and.rejectWith(new Error('ENOENT'));
394+
runCommandSpy.and.resolveTo({ stdout: '"app"', stderr: '' });
395+
396+
expect(await pm.getCurrentPackageName()).toBe('app');
397+
expect(runCommandSpy).toHaveBeenCalledWith(
398+
descriptor.binary,
399+
descriptor.getPackageNameCommand,
400+
jasmine.objectContaining({ cwd: '/repo/app/src' }),
401+
);
402+
});
403+
404+
it('should extract the package name when npm prints a single-entry workspace object', async () => {
405+
const pm = new PackageManager(host, '/repo/apps/app/src', descriptor);
406+
spyOn(host, 'readFile').and.rejectWith(new Error('ENOENT'));
407+
// Inside a workspace member, `npm pkg get name` prints `{ "app": "app" }` instead of `"app"`.
408+
runCommandSpy.and.resolveTo({ stdout: '{\n "app": "app"\n}', stderr: '' });
409+
410+
expect(await pm.getCurrentPackageName()).toBe('app');
411+
});
412+
413+
it('should return undefined when npm prints multiple workspace entries and package.json is unavailable', async () => {
414+
const pm = new PackageManager(host, '/repo', descriptor);
415+
spyOn(host, 'readFile').and.rejectWith(new Error('ENOENT'));
416+
runCommandSpy.and.resolveTo({
417+
stdout: '{\n "app-a": "app-a",\n "app-b": "app-b"\n}',
418+
stderr: '',
419+
});
420+
421+
expect(await pm.getCurrentPackageName()).toBeUndefined();
422+
});
423+
});
424+
425+
describe('getProjectDependencies', () => {
426+
it('should list the dependencies of an npm workspace member', async () => {
427+
const pm = new PackageManager(host, '/repo/apps/app', descriptor);
428+
runCommandSpy.and.resolveTo({
429+
stdout: JSON.stringify({
430+
name: 'monorepo',
431+
dependencies: {
432+
app: {
433+
version: '0.0.0',
434+
resolved: 'file:../apps/app',
435+
dependencies: { '@angular/core': { version: '22.0.0' } },
436+
},
437+
},
438+
}),
439+
stderr: '',
440+
});
441+
spyOn(host, 'readFile').and.resolveTo('{"name": "app"}');
442+
443+
const dependencies = await pm.getProjectDependencies();
444+
445+
expect([...dependencies.keys()]).toEqual(['@angular/core']);
446+
expect(dependencies.get('@angular/core')?.version).toBe('22.0.0');
447+
});
448+
});
449+
381450
describe('initializationError', () => {
382451
it('should throw initializationError when running commands', async () => {
383452
const error = new Error('Not installed');

0 commit comments

Comments
 (0)