Skip to content

Commit a457377

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 60a16dc commit a457377

File tree

3 files changed

+92
-69
lines changed

3 files changed

+92
-69
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
@@ -182,7 +182,7 @@ export const SUPPORTED_PACKAGE_MANAGERS = {
182182
configFiles: ['.yarnrc.yml', '.yarnrc.yaml'],
183183
getRegistryOptions: (registry: string) => ({ env: { YARN_NPM_REGISTRY_SERVER: registry } }),
184184
versionCommand: ['--version'],
185-
listDependenciesCommand: ['list', '--depth=0', '--json', '--recursive=false'],
185+
listDependenciesCommand: ['info', '--name-only', '--json'],
186186
getManifestCommand: ['npm', 'info', '--json'],
187187
viewCommandFieldArgFormatter: (fields) => ['--fields', fields.join(',')],
188188
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.
@@ -575,3 +507,59 @@ export function parseBunDependencies(
575507

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

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
parseNpmLikeError,
1212
parseNpmLikeManifest,
1313
parseYarnClassicError,
14+
parseYarnModernDependencies,
1415
} from './parsers';
1516

1617
describe('parsers', () => {
@@ -170,4 +171,38 @@ project node_modules
170171
expect(parseBunDependencies(stdout).size).toBe(0);
171172
});
172173
});
174+
175+
describe('parseYarnModernDependencies', () => {
176+
it('should parse yarn info --name-only --json output', () => {
177+
const stdout = `
178+
"karma@npm:6.4.4"
179+
"rxjs@npm:7.8.2"
180+
"tslib@npm:2.8.1"
181+
"typescript@patch:typescript@npm%3A5.9.3#optional!builtin<compat/typescript>::version=5.9.3&hash=5786d5"
182+
`.trim();
183+
184+
const deps = parseYarnModernDependencies(stdout);
185+
expect(deps.size).toBe(4);
186+
expect(deps.get('karma')).toEqual({ name: 'karma', version: '6.4.4' });
187+
expect(deps.get('rxjs')).toEqual({ name: 'rxjs', version: '7.8.2' });
188+
expect(deps.get('tslib')).toEqual({ name: 'tslib', version: '2.8.1' });
189+
expect(deps.get('typescript')).toEqual({
190+
name: 'typescript',
191+
version: '5.9.3',
192+
});
193+
});
194+
195+
it('should handle scoped packages', () => {
196+
const stdout = '"@angular/core@npm:20.3.15"';
197+
const deps = parseYarnModernDependencies(stdout);
198+
expect(deps.get('@angular/core')).toEqual({
199+
name: '@angular/core',
200+
version: '20.3.15',
201+
});
202+
});
203+
204+
it('should return empty map for empty stdout', () => {
205+
expect(parseYarnModernDependencies('').size).toBe(0);
206+
});
207+
});
173208
});

0 commit comments

Comments
 (0)