Skip to content

Commit b6d5789

Browse files
committed
refactor(@angular/cli): update yarn modern dependency parsing
This commit updates the dependency discovery logic for Yarn Modern (Berry) to use the `yarn info --name-only --json` command, as the `list` command is not available in newer versions of Yarn.
1 parent f9bd3b8 commit b6d5789

File tree

3 files changed

+97
-70
lines changed

3 files changed

+97
-70
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ export const SUPPORTED_PACKAGE_MANAGERS = {
167167
ignoreScriptsFlag: '--ignore-scripts',
168168
getRegistryOptions: (registry: string) => ({ env: { NPM_CONFIG_REGISTRY: registry } }),
169169
versionCommand: ['--version'],
170-
listDependenciesCommand: ['list', '--depth=0', '--json', '--recursive=false'],
170+
listDependenciesCommand: ['info', '--name-only', '--json'],
171171
getManifestCommand: ['npm', 'info', '--json'],
172172
viewCommandFieldArgFormatter: (fields) => ['--fields', fields.join(',')],
173173
outputParsers: {

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

Lines changed: 56 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -170,74 +170,6 @@ export function parseYarnClassicDependencies(
170170
return dependencies;
171171
}
172172

173-
/**
174-
* Parses the output of `yarn list` (modern).
175-
*
176-
* The expected JSON structure is a single object.
177-
* Yarn modern does not provide a path, so the `path` property will be `undefined`.
178-
*
179-
* ```json
180-
* {
181-
* "trees": [
182-
* { "name": "@angular/cli@18.0.0", "children": [] }
183-
* ]
184-
* }
185-
* ```
186-
*
187-
* @param stdout The standard output of the command.
188-
* @param logger An optional logger instance.
189-
* @returns A map of package names to their installed package details.
190-
*/
191-
export function parseYarnModernDependencies(
192-
stdout: string,
193-
logger?: Logger,
194-
): Map<string, InstalledPackage> {
195-
logger?.debug(`Parsing yarn modern dependency list...`);
196-
logStdout(stdout, logger);
197-
198-
const dependencies = new Map<string, InstalledPackage>();
199-
if (!stdout) {
200-
logger?.debug(' stdout is empty. No dependencies found.');
201-
202-
return dependencies;
203-
}
204-
205-
// Modern yarn `list` command outputs a single JSON object with a `trees` property.
206-
// Each line is not a separate JSON object.
207-
try {
208-
const data = JSON.parse(stdout);
209-
for (const info of data.trees) {
210-
const name = info.name.split('@')[0];
211-
const version = info.name.split('@').pop();
212-
dependencies.set(name, {
213-
name,
214-
version,
215-
});
216-
}
217-
} catch (e) {
218-
logger?.debug(
219-
` Failed to parse as single JSON object: ${e}. Falling back to line-by-line parsing.`,
220-
);
221-
// Fallback for older versions of yarn berry that might still output json lines
222-
for (const json of parseJsonLines(stdout, logger)) {
223-
if (json.type === 'tree' && json.data?.trees) {
224-
for (const info of json.data.trees) {
225-
const name = info.name.split('@')[0];
226-
const version = info.name.split('@').pop();
227-
dependencies.set(name, {
228-
name,
229-
version,
230-
});
231-
}
232-
}
233-
}
234-
}
235-
236-
logger?.debug(` Found ${dependencies.size} dependencies.`);
237-
238-
return dependencies;
239-
}
240-
241173
/**
242174
* Parses the output of `npm view` or a compatible command to get a package manifest.
243175
* @param stdout The standard output of the command.
@@ -573,3 +505,59 @@ export function parseBunDependencies(
573505

574506
return dependencies;
575507
}
508+
509+
/**
510+
* Parses the output of `yarn info --name-only --json`.
511+
*
512+
* The expected output is a JSON stream (JSONL) of strings.
513+
* Each string represents a package locator.
514+
*
515+
* ```
516+
* "karma@npm:6.4.4"
517+
* "@angular/core@npm:20.3.15"
518+
* ```
519+
*
520+
* @param stdout The standard output of the command.
521+
* @param logger An optional logger instance.
522+
* @returns A map of package names to their installed package details.
523+
*/
524+
export function parseYarnModernDependencies(
525+
stdout: string,
526+
logger?: Logger,
527+
): Map<string, InstalledPackage> {
528+
logger?.debug('Parsing Yarn Berry dependency list...');
529+
logStdout(stdout, logger);
530+
531+
const dependencies = new Map<string, InstalledPackage>();
532+
if (!stdout) {
533+
return dependencies;
534+
}
535+
536+
for (const json of parseJsonLines(stdout, logger)) {
537+
if (typeof json === 'string') {
538+
const match = json.match(/^(@?[^@]+)@(.+)$/);
539+
if (match) {
540+
const name = match[1];
541+
let version = match[2];
542+
543+
// Handle "npm:" prefix
544+
if (version.startsWith('npm:')) {
545+
version = version.slice(4);
546+
}
547+
548+
// Handle complex locators with embedded version metadata (e.g., "patch:...", "virtual:...")
549+
// Yarn Berry often appends metadata like "::version=x.y.z"
550+
const versionParamMatch = version.match(/::version=([^&]+)/);
551+
if (versionParamMatch) {
552+
version = versionParamMatch[1];
553+
}
554+
555+
dependencies.set(name, { name, version });
556+
}
557+
}
558+
}
559+
560+
logger?.debug(` Found ${dependencies.size} dependencies.`);
561+
562+
return dependencies;
563+
}

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

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import { parseBunDependencies, parseNpmLikeError, parseYarnClassicError } from './parsers';
9+
import {
10+
parseBunDependencies,
11+
parseNpmLikeError,
12+
parseYarnClassicError,
13+
parseYarnModernDependencies,
14+
} from './parsers';
1015

1116
describe('parsers', () => {
1217
describe('parseNpmLikeError', () => {
@@ -146,4 +151,38 @@ project node_modules
146151
expect(parseBunDependencies(stdout).size).toBe(0);
147152
});
148153
});
154+
155+
describe('parseYarnModernDependencies', () => {
156+
it('should parse yarn info --name-only --json output', () => {
157+
const stdout = `
158+
"karma@npm:6.4.4"
159+
"rxjs@npm:7.8.2"
160+
"tslib@npm:2.8.1"
161+
"typescript@patch:typescript@npm%3A5.9.3#optional!builtin<compat/typescript>::version=5.9.3&hash=5786d5"
162+
`.trim();
163+
164+
const deps = parseYarnModernDependencies(stdout);
165+
expect(deps.size).toBe(4);
166+
expect(deps.get('karma')).toEqual({ name: 'karma', version: '6.4.4' });
167+
expect(deps.get('rxjs')).toEqual({ name: 'rxjs', version: '7.8.2' });
168+
expect(deps.get('tslib')).toEqual({ name: 'tslib', version: '2.8.1' });
169+
expect(deps.get('typescript')).toEqual({
170+
name: 'typescript',
171+
version: '5.9.3',
172+
});
173+
});
174+
175+
it('should handle scoped packages', () => {
176+
const stdout = '"@angular/core@npm:20.3.15"';
177+
const deps = parseYarnModernDependencies(stdout);
178+
expect(deps.get('@angular/core')).toEqual({
179+
name: '@angular/core',
180+
version: '20.3.15',
181+
});
182+
});
183+
184+
it('should return empty map for empty stdout', () => {
185+
expect(parseYarnModernDependencies('').size).toBe(0);
186+
});
187+
});
149188
});

0 commit comments

Comments
 (0)