From efaf06fa7a500d15780c75a10007af97ab9a8db5 Mon Sep 17 00:00:00 2001 From: MeAkib Date: Fri, 30 Oct 2026 18:39:10 +0600 Subject: [PATCH 1/2] fix(@angular/cli): handle modern yarn registry error output and 404 detection Modern yarn (v2+) reports registry errors as JSON lines on `stdout`, one per message line, such as `{"type":"error","displayName":"YN0035","data":"Package not found"}` followed by a `Response Code: 404 (Not Found)` line. Because `parseNpmLikeError` did not recognize this format, querying a package that does not exist under modern yarn threw the original `Process exited with code 1.` error instead of returning `null`. Add a `parseYarnModernError` parser for the modern yarn descriptor that reads these JSON lines and returns the HTTP status as the error code, so not-found responses are normalized to `E404`. --- .../package-manager-descriptor.ts | 3 +- .../package-managers/package-manager_spec.ts | 17 ++++++ .../cli/src/package-managers/parsers.ts | 52 +++++++++++++++++++ .../cli/src/package-managers/parsers_spec.ts | 28 ++++++++++ 4 files changed, 99 insertions(+), 1 deletion(-) 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..3f55bd5d7f42 100644 --- a/packages/angular/cli/src/package-managers/parsers.ts +++ b/packages/angular/cli/src/package-managers/parsers.ts @@ -636,6 +636,58 @@ 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()).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 = ` From b79c42fe7a01e1b535127fe41002312ee20c411b Mon Sep 17 00:00:00 2001 From: MeAkib Date: Fri, 30 Oct 2026 18:47:44 +0600 Subject: [PATCH 2/2] fixup! fix(@angular/cli): handle modern yarn registry error output and 404 detection --- packages/angular/cli/src/package-managers/parsers.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/angular/cli/src/package-managers/parsers.ts b/packages/angular/cli/src/package-managers/parsers.ts index 3f55bd5d7f42..b6a9a3f5cbb7 100644 --- a/packages/angular/cli/src/package-managers/parsers.ts +++ b/packages/angular/cli/src/package-managers/parsers.ts @@ -684,7 +684,11 @@ export function parseYarnModernError(output: string, logger?: Logger): ErrorInfo return { code, summary: first.data.trim(), - detail: rest.map(({ data }) => data.trim()).join('\n') || undefined, + detail: + rest + .map(({ data }) => data.trim()) + .filter(Boolean) + .join('\n') || undefined, }; }