Skip to content

Commit 2085fdb

Browse files
committed
fix(@angular/cli): introduce initial package manager workspace awareness
This change adds the getCurrentPackageName method to the package manager abstraction. When dealing with workspaces (such as npm workspaces), the parser uses the resolved identity of the active package to prioritize dependencies belonging directly to that subproject. It ensures that running ng update inside a subproject directory resolves the dependency versions declared for that subproject, while gracefully falling back to root hoisted versions for shared dependencies.
1 parent 73233dc commit 2085fdb

4 files changed

Lines changed: 133 additions & 8 deletions

File tree

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ export interface PackageManagerDescriptor {
8787
/** The command to list all installed dependencies. */
8888
readonly listDependenciesCommand: readonly string[];
8989

90+
/** The command to get the current package name. */
91+
readonly getPackageNameCommand?: readonly string[];
92+
9093
/** The command to fetch the registry manifest of a package. */
9194
readonly getManifestCommand: readonly string[];
9295

@@ -99,7 +102,11 @@ export interface PackageManagerDescriptor {
99102
/** A collection of functions to parse the output of specific commands. */
100103
readonly outputParsers: {
101104
/** A function to parse the output of `listDependenciesCommand`. */
102-
listDependencies: (stdout: string, logger?: Logger) => Map<string, InstalledPackage>;
105+
listDependencies: (
106+
stdout: string,
107+
logger?: Logger,
108+
options?: { workspacePackageName?: string },
109+
) => Map<string, InstalledPackage>;
103110

104111
/** A function to parse the output of `getManifestCommand` for a specific version. */
105112
getRegistryManifest: (stdout: string, logger?: Logger) => PackageManifest | null;
@@ -158,6 +165,7 @@ export const SUPPORTED_PACKAGE_MANAGERS = {
158165
getRegistryOptions: (registry: string) => ({ args: ['--registry', registry] }),
159166
versionCommand: ['--version'],
160167
listDependenciesCommand: ['list', '--depth=0', '--json=true', '--all=true'],
168+
getPackageNameCommand: ['pkg', 'get', 'name'],
161169
getManifestCommand: ['view', '--json'],
162170
viewCommandFieldArgFormatter: (fields) => [...fields],
163171
outputParsers: {
@@ -237,6 +245,7 @@ export const SUPPORTED_PACKAGE_MANAGERS = {
237245
getRegistryOptions: (registry: string) => ({ args: ['--registry', registry] }),
238246
versionCommand: ['--version'],
239247
listDependenciesCommand: ['list', '--depth=0', '--json'],
248+
getPackageNameCommand: ['pkg', 'get', 'name'],
240249
getManifestCommand: ['view', '--json'],
241250
viewCommandFieldArgFormatter: (fields) => [...fields],
242251
outputParsers: {

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

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,9 @@ export class PackageManager {
145145

146146
const args = this.descriptor.listDependenciesCommand;
147147

148+
const workspacePackageName = await this.getCurrentPackageName();
148149
const dependencies = await this.#fetchAndParse(args, (stdout, logger) =>
149-
this.descriptor.outputParsers.listDependencies(stdout, logger),
150+
this.descriptor.outputParsers.listDependencies(stdout, logger, { workspacePackageName }),
150151
);
151152

152153
return (this.#dependencyCache = dependencies ?? new Map());
@@ -361,6 +362,31 @@ export class PackageManager {
361362
this.#dependencyCache = null;
362363
}
363364

365+
/**
366+
* Gets the name of the package in the current project.
367+
*/
368+
async getCurrentPackageName(): Promise<string | undefined> {
369+
if (this.descriptor.getPackageNameCommand) {
370+
try {
371+
const { stdout } = await this.#run(this.descriptor.getPackageNameCommand);
372+
if (stdout) {
373+
return JSON.parse(stdout);
374+
}
375+
} catch {
376+
// Fall back to reading file if command fails
377+
}
378+
}
379+
380+
try {
381+
const content = await this.host.readFile(join(this.cwd, 'package.json'));
382+
const pkgJson = JSON.parse(content);
383+
384+
return pkgJson.name;
385+
} catch {
386+
return undefined;
387+
}
388+
}
389+
364390
/**
365391
* Gets the version of the package manager binary.
366392
*/

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

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ interface NpmListDependency {
8181
export function parseNpmLikeDependencies(
8282
stdout: string,
8383
logger?: Logger,
84+
options?: { workspacePackageName?: string },
8485
): Map<string, InstalledPackage> {
8586
logger?.debug(`Parsing npm-like dependency list...`);
8687
logStdout(stdout, logger);
@@ -108,13 +109,56 @@ export function parseNpmLikeDependencies(
108109
return dependencies;
109110
}
110111

112+
const workspacePackageName = options?.workspacePackageName;
113+
114+
if (workspacePackageName) {
115+
for (const dependencyMap of dependencyMaps) {
116+
const info = dependencyMap[workspacePackageName];
117+
if (info && typeof info === 'object') {
118+
const nestedMaps = [
119+
info.dependencies,
120+
info.devDependencies,
121+
info.unsavedDependencies,
122+
].filter((d) => !!d) as Record<string, NpmListDependency>[];
123+
124+
for (const nestedMap of nestedMaps) {
125+
for (const [name, nestedInfo] of Object.entries(nestedMap)) {
126+
if (nestedInfo && typeof nestedInfo === 'object' && nestedInfo.version) {
127+
dependencies.set(name, {
128+
name,
129+
version: nestedInfo.version,
130+
path: nestedInfo.path,
131+
});
132+
}
133+
}
134+
}
135+
}
136+
}
137+
}
138+
139+
// Extract top-level dependencies (root), without overwriting subproject dependencies
111140
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-
});
141+
for (const [name, info] of Object.entries(
142+
dependencyMap as Record<string, NpmListDependency & { resolved?: string }>,
143+
)) {
144+
if (!info || typeof info !== 'object') {
145+
continue;
146+
}
147+
148+
// Exclude local monorepo workspace packages (which originate from a local file/dir
149+
// and contain nested dependency maps in the output of `npm list --depth=0`),
150+
// while preserving third-party packages installed from local paths.
151+
const isWorkspacePackage =
152+
info.resolved?.startsWith('file:') &&
153+
(!!info.dependencies || !!info.devDependencies || !!info.unsavedDependencies);
154+
155+
if (info.version && !dependencies.has(name) && !isWorkspacePackage) {
156+
dependencies.set(name, {
157+
name,
158+
version: info.version,
159+
path: info.path,
160+
});
161+
}
118162
}
119163
}
120164

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

Lines changed: 46 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,51 @@ 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 dependencies from current subproject and hoisted root', () => {
35+
const stdout = JSON.stringify({
36+
version: '1.0.0',
37+
name: 'monorepo-root',
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+
typescript: {
49+
version: '5.9.3',
50+
},
51+
},
52+
});
53+
54+
const deps = parseNpmLikeDependencies(stdout, undefined, { workspacePackageName: 'app' });
55+
expect(deps.size).toBe(2);
56+
expect(deps.get('rxjs')).toEqual({ name: 'rxjs', version: '7.8.1', path: undefined });
57+
expect(deps.get('typescript')).toEqual({
58+
name: 'typescript',
59+
version: '5.9.3',
60+
path: undefined,
61+
});
62+
});
63+
});
64+
1965
describe('parseNpmLikeError', () => {
2066
it('should parse a structured JSON error from modern yarn', () => {
2167
const stdout = JSON.stringify({

0 commit comments

Comments
 (0)