Skip to content

Commit 951a7c1

Browse files
committed
fix(@angular/cli): support npm 12 metadata array and error formats
In npm 12, `npm view --json <packageName> <fields>` returns package metadata formatted as an array of objects rather than a single JSON object. This caused `metadata.versions` to evaluate to `undefined` and throw `Cannot read properties of undefined (reading 'filter')` during `ng update`. Additionally, npm 12 wraps JSON error output inside an `error` property (`{ "error": { "code": ... } }`), which is now unwrapped by `parseNpmLikeError`. Closes #33679
1 parent bab3c53 commit 951a7c1

2 files changed

Lines changed: 124 additions & 6 deletions

File tree

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

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -262,13 +262,26 @@ function isValidManifest(obj: unknown): obj is PackageManifest {
262262
return false;
263263
}
264264

265-
const record = obj as Record<string, unknown>;
266-
const name = record.name;
267-
const version = record.version;
265+
const { name, version } = obj as Record<string, unknown>;
268266

269267
return typeof name === 'string' && typeof version === 'string' && valid(version) !== null;
270268
}
271269

270+
function isValidMetadata(obj: unknown): obj is PackageMetadata {
271+
if (typeof obj !== 'object' || obj === null) {
272+
return false;
273+
}
274+
275+
const { name, versions, 'dist-tags': distTags } = obj as Record<string, unknown>;
276+
277+
return (
278+
typeof name === 'string' &&
279+
Array.isArray(versions) &&
280+
typeof distTags === 'object' &&
281+
distTags !== null
282+
);
283+
}
284+
272285
/**
273286
* Parses the output of `npm view` or a compatible command to get a package manifest.
274287
* @param stdout The standard output of the command.
@@ -340,7 +353,28 @@ export function parseNpmLikeMetadata(stdout: string, logger?: Logger): PackageMe
340353
return null;
341354
}
342355

343-
return JSON.parse(stdout);
356+
const result = JSON.parse(stdout);
357+
358+
// npm 12+ `npm view --json` always returns an array of objects.
359+
if (Array.isArray(result)) {
360+
for (const item of result) {
361+
if (isValidMetadata(item)) {
362+
return item;
363+
}
364+
}
365+
366+
logger?.debug(' No valid metadata found in the array.');
367+
368+
return null;
369+
}
370+
371+
if (!isValidMetadata(result)) {
372+
logger?.debug(' Parsed JSON is not valid metadata (missing name, versions, or dist-tags).');
373+
374+
return null;
375+
}
376+
377+
return result;
344378
}
345379

346380
/**
@@ -479,9 +513,16 @@ export function parseNpmLikeError(output: string, logger?: Logger): ErrorInfo |
479513
return null;
480514
}
481515

482-
// Attempt to parse as JSON first (common for pnpm, modern yarn, bun)
516+
// Attempt to parse as JSON first (common for pnpm, modern yarn, bun, npm 12)
483517
try {
484-
const jsonError = JSON.parse(output);
518+
let jsonError = JSON.parse(output);
519+
if (Array.isArray(jsonError)) {
520+
jsonError = jsonError[0];
521+
}
522+
if (jsonError && typeof jsonError === 'object' && 'error' in jsonError) {
523+
jsonError = jsonError.error;
524+
}
525+
485526
if (
486527
jsonError &&
487528
typeof jsonError.code === 'string' &&

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

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
parseNpmLikeDependencies,
1313
parseNpmLikeError,
1414
parseNpmLikeManifest,
15+
parseNpmLikeMetadata,
1516
parsePnpmReleaseAge,
1617
parseYarnClassicDependencies,
1718
parseYarnClassicError,
@@ -231,6 +232,22 @@ describe('parsers', () => {
231232
const error = parseNpmLikeError('An unexpected error occurred.');
232233
expect(error).toBeNull();
233234
});
235+
236+
it('should parse a structured JSON error wrapped in an error property (npm 12+ format)', () => {
237+
const stdout = JSON.stringify({
238+
error: {
239+
code: 'E404',
240+
summary: 'Not Found',
241+
detail: 'Package not found.',
242+
},
243+
});
244+
const error = parseNpmLikeError(stdout);
245+
expect(error).toEqual({
246+
code: 'E404',
247+
summary: 'Not Found',
248+
detail: 'Package not found.',
249+
});
250+
});
234251
});
235252

236253
describe('parseNpmLikeManifest', () => {
@@ -295,6 +312,66 @@ describe('parsers', () => {
295312
});
296313
});
297314

315+
describe('parseNpmLikeMetadata', () => {
316+
it('should parse a single metadata object', () => {
317+
const stdout = JSON.stringify({
318+
name: 'foo',
319+
'dist-tags': { latest: '1.0.0' },
320+
versions: ['1.0.0'],
321+
});
322+
expect(parseNpmLikeMetadata(stdout)).toEqual({
323+
name: 'foo',
324+
'dist-tags': { latest: '1.0.0' },
325+
versions: ['1.0.0'],
326+
});
327+
});
328+
329+
it('should parse metadata from an array (npm 12+ format)', () => {
330+
const stdout = JSON.stringify([
331+
{
332+
name: 'foo',
333+
'dist-tags': { latest: '1.0.0' },
334+
versions: ['1.0.0'],
335+
},
336+
]);
337+
expect(parseNpmLikeMetadata(stdout)).toEqual({
338+
name: 'foo',
339+
'dist-tags': { latest: '1.0.0' },
340+
versions: ['1.0.0'],
341+
});
342+
});
343+
344+
it('should return the first valid metadata from an array', () => {
345+
const stdout = JSON.stringify([
346+
{ name: 'foo' }, // Invalid (missing versions and dist-tags)
347+
{
348+
name: 'foo',
349+
'dist-tags': { latest: '1.0.0' },
350+
versions: ['1.0.0'],
351+
},
352+
]);
353+
expect(parseNpmLikeMetadata(stdout)).toEqual({
354+
name: 'foo',
355+
'dist-tags': { latest: '1.0.0' },
356+
versions: ['1.0.0'],
357+
});
358+
});
359+
360+
it('should return null if no valid metadata is found in an array', () => {
361+
const stdout = JSON.stringify([{ name: 'foo' }, { versions: ['1.0.0'] }]);
362+
expect(parseNpmLikeMetadata(stdout)).toBeNull();
363+
});
364+
365+
it('should return null for invalid single object', () => {
366+
const stdout = JSON.stringify({ name: 'foo' }); // Missing versions and dist-tags
367+
expect(parseNpmLikeMetadata(stdout)).toBeNull();
368+
});
369+
370+
it('should return null for empty stdout', () => {
371+
expect(parseNpmLikeMetadata('')).toBeNull();
372+
});
373+
});
374+
298375
describe('parseYarnClassicManifest', () => {
299376
it('should parse a valid manifest', () => {
300377
const stdout = JSON.stringify({

0 commit comments

Comments
 (0)