diff --git a/packages/angular/cli/src/package-managers/package-manager-descriptor.ts b/packages/angular/cli/src/package-managers/package-manager-descriptor.ts index 4aed574cfb19..edf2e1830d87 100644 --- a/packages/angular/cli/src/package-managers/package-manager-descriptor.ts +++ b/packages/angular/cli/src/package-managers/package-manager-descriptor.ts @@ -30,6 +30,7 @@ import { parseYarnClassicManifest, parseYarnClassicMetadata, parseYarnModernDependencies, + parseYarnModernError, parseYarnReleaseAge, } from './parsers'; @@ -231,7 +232,7 @@ export const SUPPORTED_PACKAGE_MANAGERS = { listDependencies: parseYarnModernDependencies, getRegistryManifest: parseNpmLikeManifest, getRegistryMetadata: parseNpmLikeMetadata, - getError: parseNpmLikeError, + getError: parseYarnModernError, getReleaseAge: parseYarnReleaseAge, }, isNotFound: isKnownNotFound, 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 5a32562989ee..e6b8f95c0c4c 100644 --- a/packages/angular/cli/src/package-managers/package-manager_spec.ts +++ b/packages/angular/cli/src/package-managers/package-manager_spec.ts @@ -422,6 +422,23 @@ describe('PackageManager', () => { expect(await pm.getRegistryMetadata('non-existent')).toBeNull(); }); + + it('should return null when yarn npm info fails with a missing package', async () => { + const pm = new PackageManager(host, '/tmp', SUPPORTED_PACKAGE_MANAGERS['yarn']); + runCommandSpy.and.rejectWith( + new PackageManagerError( + 'Process exited with code 1.', + [ + '{"type":"error","name":35,"displayName":"YN0035","indent":"","data":"Package not found"}', + '{"type":"error","name":35,"displayName":"YN0035","indent":"","data":" Response Code: 404 (Not Found)"}', + ].join('\n'), + '', + 1, + ), + ); + + expect(await pm.getRegistryMetadata('non-existent')).toBeNull(); + }); }); describe('getCurrentPackageName', () => { diff --git a/packages/angular/cli/src/package-managers/parsers.ts b/packages/angular/cli/src/package-managers/parsers.ts index eedfb7ce8471..b6a9a3f5cbb7 100644 --- a/packages/angular/cli/src/package-managers/parsers.ts +++ b/packages/angular/cli/src/package-managers/parsers.ts @@ -636,6 +636,62 @@ export function parseYarnClassicError(output: string, logger?: Logger): ErrorInf return null; } +/** + * Parses the `stdout` or `stderr` output of modern yarn (v2+) to extract structured error information. + * + * Modern yarn reports errors as JSON lines, one per message line. The first line holds the summary, + * and registry errors include the HTTP status, which is returned as a standardized error code + * (`E${statusCode}`). Other output is delegated to `parseNpmLikeError`. + * + * Example output (`yarn npm info --json`): + * ```json + * {"type":"error","name":35,"displayName":"YN0035","indent":"","data":"Package not found"} + * {"type":"error","name":35,"displayName":"YN0035","indent":"","data":" Response Code: 404 (Not Found)"} + * ``` + * + * @param output The standard output or standard error of the command. + * @param logger An optional logger instance. + * @returns An `ErrorInfo` object if parsing is successful, otherwise `null`. + */ +export function parseYarnModernError(output: string, logger?: Logger): ErrorInfo | null { + logger?.debug(`Parsing modern yarn error output...`); + logStdout(output, logger); // Log output for debugging purposes + + if (!output) { + logger?.debug(' output is empty. No error found.'); + + return null; + } + + const messages: { code: string; data: string }[] = []; + for (const json of parseJsonLines(output, logger)) { + if (json?.type === 'error' && typeof json.data === 'string') { + messages.push({ code: String(json.displayName ?? 'UNKNOWN_ERROR'), data: json.data }); + } + } + + if (messages.length === 0) { + return parseNpmLikeError(output, logger); + } + + const [first, ...rest] = messages; + const statusCodeMatch = messages + .map(({ data }) => data.match(/Response Code: (\d{3})/)) + .find((match) => match !== null); + const code = statusCodeMatch ? `E${statusCodeMatch[1]}` : first.code; + logger?.debug(` Successfully parsed modern yarn error with code '${code}'.`); + + return { + code, + summary: first.data.trim(), + detail: + rest + .map(({ data }) => data.trim()) + .filter(Boolean) + .join('\n') || undefined, + }; +} + /** * Known Bun JSON error messages that indicate a requested version or dist-tag does not exist. */ diff --git a/packages/angular/cli/src/package-managers/parsers_spec.ts b/packages/angular/cli/src/package-managers/parsers_spec.ts index 93c3fd66a9f1..930e3c49e6a9 100644 --- a/packages/angular/cli/src/package-managers/parsers_spec.ts +++ b/packages/angular/cli/src/package-managers/parsers_spec.ts @@ -19,6 +19,7 @@ import { parseYarnClassicError, parseYarnClassicManifest, parseYarnModernDependencies, + parseYarnModernError, parseYarnReleaseAge, } from './parsers'; @@ -641,6 +642,33 @@ project node_modules }); }); + describe('parseYarnModernError', () => { + it('should parse a missing package from yarn npm info as E404', () => { + const stdout = [ + '{"type":"error","name":35,"displayName":"YN0035","indent":"","data":"Package not found"}', + '{"type":"error","name":35,"displayName":"YN0035","indent":"","data":" Response Code: 404 (Not Found)"}', + '{"type":"error","name":35,"displayName":"YN0035","indent":"","data":" Request Method: GET"}', + '{"type":"error","name":35,"displayName":"YN0035","indent":"","data":" Request URL: https://registry.yarnpkg.com/@angular%2fnon-existent"}', + ].join('\n'); + expect(parseYarnModernError(stdout)).toEqual( + jasmine.objectContaining({ code: 'E404', summary: 'Package not found' }), + ); + }); + + it('should use the yarn error code when there is no HTTP status', () => { + const stdout = + '{"type":"error","name":35,"displayName":"YN0035","indent":"","data":"Request timed out"}'; + expect(parseYarnModernError(stdout)).toEqual( + jasmine.objectContaining({ code: 'YN0035', summary: 'Request timed out' }), + ); + }); + + it('should return null for successful or empty output', () => { + expect(parseYarnModernError('{"name":"@angular/core","version":"22.2.4"}')).toBeNull(); + expect(parseYarnModernError('')).toBeNull(); + }); + }); + describe('parseYarnModernDependencies', () => { it('should parse yarn info --name-only --json output', () => { const stdout = `