Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
parseYarnClassicManifest,
parseYarnClassicMetadata,
parseYarnModernDependencies,
parseYarnModernError,
parseYarnReleaseAge,
} from './parsers';

Expand Down Expand Up @@ -231,7 +232,7 @@ export const SUPPORTED_PACKAGE_MANAGERS = {
listDependencies: parseYarnModernDependencies,
getRegistryManifest: parseNpmLikeManifest,
getRegistryMetadata: parseNpmLikeMetadata,
getError: parseNpmLikeError,
getError: parseYarnModernError,
getReleaseAge: parseYarnReleaseAge,
},
isNotFound: isKnownNotFound,
Expand Down
17 changes: 17 additions & 0 deletions packages/angular/cli/src/package-managers/package-manager_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
56 changes: 56 additions & 0 deletions packages/angular/cli/src/package-managers/parsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <missing-package> --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);
Comment thread
alan-agius4 marked this conversation as resolved.
Resolved
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.
*/
Expand Down
28 changes: 28 additions & 0 deletions packages/angular/cli/src/package-managers/parsers_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
parseYarnClassicError,
parseYarnClassicManifest,
parseYarnModernDependencies,
parseYarnModernError,
parseYarnReleaseAge,
} from './parsers';

Expand Down Expand Up @@ -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 = `
Expand Down
Loading