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
53 changes: 47 additions & 6 deletions packages/angular/cli/src/package-managers/parsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,13 +262,26 @@ function isValidManifest(obj: unknown): obj is PackageManifest {
return false;
}

const record = obj as Record<string, unknown>;
const name = record.name;
const version = record.version;
const { name, version } = obj as Record<string, unknown>;

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

function isValidMetadata(obj: unknown): obj is PackageMetadata {
if (typeof obj !== 'object' || obj === null) {
return false;
}

const { name, versions, 'dist-tags': distTags } = obj as Record<string, unknown>;

return (
typeof name === 'string' &&
Array.isArray(versions) &&
typeof distTags === 'object' &&
distTags !== null
);
}

/**
* Parses the output of `npm view` or a compatible command to get a package manifest.
* @param stdout The standard output of the command.
Expand Down Expand Up @@ -340,7 +353,28 @@ export function parseNpmLikeMetadata(stdout: string, logger?: Logger): PackageMe
return null;
}

return JSON.parse(stdout);
const result = JSON.parse(stdout);

// npm 12+ `npm view --json` always returns an array of objects.
if (Array.isArray(result)) {
for (const item of result) {
if (isValidMetadata(item)) {
return item;
}
}

logger?.debug(' No valid metadata found in the array.');

return null;
}

if (!isValidMetadata(result)) {
logger?.debug(' Parsed JSON is not valid metadata (missing name, versions, or dist-tags).');

return null;
}

return result;
}

/**
Expand Down Expand Up @@ -479,9 +513,16 @@ export function parseNpmLikeError(output: string, logger?: Logger): ErrorInfo |
return null;
}

// Attempt to parse as JSON first (common for pnpm, modern yarn, bun)
// Attempt to parse as JSON first (common for pnpm, modern yarn, bun, npm 12)
try {
const jsonError = JSON.parse(output);
let jsonError = JSON.parse(output);
if (Array.isArray(jsonError)) {
jsonError = jsonError[0];
}
if (jsonError && typeof jsonError === 'object' && 'error' in jsonError) {
jsonError = jsonError.error;
}

if (
jsonError &&
typeof jsonError.code === 'string' &&
Expand Down
111 changes: 111 additions & 0 deletions packages/angular/cli/src/package-managers/parsers_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
parseNpmLikeDependencies,
parseNpmLikeError,
parseNpmLikeManifest,
parseNpmLikeMetadata,
parsePnpmReleaseAge,
parseYarnClassicDependencies,
parseYarnClassicError,
Expand Down Expand Up @@ -231,6 +232,56 @@ describe('parsers', () => {
const error = parseNpmLikeError('An unexpected error occurred.');
expect(error).toBeNull();
});

it('should parse a structured JSON error wrapped in an error property (npm 12+ format)', () => {
const stdout = JSON.stringify({
error: {
code: 'E404',
summary: 'Not Found',
detail: 'Package not found.',
},
});
const error = parseNpmLikeError(stdout);
expect(error).toEqual({
code: 'E404',
summary: 'Not Found',
detail: 'Package not found.',
});
});
Comment thread
alan-agius4 marked this conversation as resolved.

it('should parse a structured JSON error wrapped in an array', () => {
const stdout = JSON.stringify([
{
code: 'E404',
summary: 'Not Found',
detail: 'Package not found.',
},
]);
const error = parseNpmLikeError(stdout);
expect(error).toEqual({
code: 'E404',
summary: 'Not Found',
detail: 'Package not found.',
});
});

it('should parse a structured JSON error wrapped in both an array and an error property', () => {
const stdout = JSON.stringify([
{
error: {
code: 'E404',
summary: 'Not Found',
detail: 'Package not found.',
},
},
]);
const error = parseNpmLikeError(stdout);
expect(error).toEqual({
code: 'E404',
summary: 'Not Found',
detail: 'Package not found.',
});
});
});

describe('parseNpmLikeManifest', () => {
Expand Down Expand Up @@ -295,6 +346,66 @@ describe('parsers', () => {
});
});

describe('parseNpmLikeMetadata', () => {
it('should parse a single metadata object', () => {
const stdout = JSON.stringify({
name: 'foo',
'dist-tags': { latest: '1.0.0' },
versions: ['1.0.0'],
});
expect(parseNpmLikeMetadata(stdout)).toEqual({
name: 'foo',
'dist-tags': { latest: '1.0.0' },
versions: ['1.0.0'],
});
});

it('should parse metadata from an array (npm 12+ format)', () => {
const stdout = JSON.stringify([
{
name: 'foo',
'dist-tags': { latest: '1.0.0' },
versions: ['1.0.0'],
},
]);
expect(parseNpmLikeMetadata(stdout)).toEqual({
name: 'foo',
'dist-tags': { latest: '1.0.0' },
versions: ['1.0.0'],
});
});

it('should return the first valid metadata from an array', () => {
const stdout = JSON.stringify([
{ name: 'foo' }, // Invalid (missing versions and dist-tags)
{
name: 'foo',
'dist-tags': { latest: '1.0.0' },
versions: ['1.0.0'],
},
]);
expect(parseNpmLikeMetadata(stdout)).toEqual({
name: 'foo',
'dist-tags': { latest: '1.0.0' },
versions: ['1.0.0'],
});
});

it('should return null if no valid metadata is found in an array', () => {
const stdout = JSON.stringify([{ name: 'foo' }, { versions: ['1.0.0'] }]);
expect(parseNpmLikeMetadata(stdout)).toBeNull();
});

it('should return null for invalid single object', () => {
const stdout = JSON.stringify({ name: 'foo' }); // Missing versions and dist-tags
expect(parseNpmLikeMetadata(stdout)).toBeNull();
});

it('should return null for empty stdout', () => {
expect(parseNpmLikeMetadata('')).toBeNull();
});
});

describe('parseYarnClassicManifest', () => {
it('should parse a valid manifest', () => {
const stdout = JSON.stringify({
Expand Down
Loading