Skip to content

Commit c6cf19b

Browse files
committed
refactor(@angular/cli): add validation and logging to npm manifest parsing
Introduce a reusable `isValidManifest` type guard to ensure that parsed manifests contain both `name` and `version` strings. This applies to both single object responses and elements within an array response from the package manager.
1 parent 5c1db4e commit c6cf19b

2 files changed

Lines changed: 67 additions & 2 deletions

File tree

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

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { ErrorInfo } from './error';
1616
import { Logger } from './logger';
1717
import { PackageManifest, PackageMetadata } from './package-metadata';
1818
import { InstalledPackage } from './package-tree';
19-
import { compare } from 'semver';
19+
import { compare, valid } from 'semver';
2020

2121
const MAX_LOG_LENGTH = 1024;
2222

@@ -217,6 +217,22 @@ export function parseYarnClassicDependencies(
217217
return dependencies;
218218
}
219219

220+
function isValidManifest(obj: unknown): obj is PackageManifest {
221+
if (typeof obj !== 'object' || obj === null) {
222+
return false;
223+
}
224+
225+
const record = obj as Record<string, unknown>;
226+
const name = record.name;
227+
const version = record.version;
228+
229+
return (
230+
typeof name === 'string' &&
231+
typeof version === 'string' &&
232+
valid(version) !== null
233+
);
234+
}
235+
220236
/**
221237
* Parses the output of `npm view` or a compatible command to get a package manifest.
222238
* @param stdout The standard output of the command.
@@ -242,7 +258,8 @@ export function parseNpmLikeManifest(stdout: string, logger?: Logger): PackageMa
242258
let maxManifest: PackageManifest | null = null;
243259

244260
for (const manifest of result) {
245-
if (!manifest || typeof manifest.version !== 'string') {
261+
if (!isValidManifest(manifest)) {
262+
logger?.debug(' Skipping invalid manifest in array (missing name, version, or invalid SemVer).');
246263
continue;
247264
}
248265

@@ -251,9 +268,19 @@ export function parseNpmLikeManifest(stdout: string, logger?: Logger): PackageMa
251268
}
252269
}
253270

271+
if (!maxManifest) {
272+
logger?.debug(' No valid manifests found in the array.');
273+
}
274+
254275
return maxManifest;
255276
}
256277

278+
if (!isValidManifest(result)) {
279+
logger?.debug(' Parsed JSON is not a valid manifest (missing name, version, or invalid SemVer).');
280+
281+
return null;
282+
}
283+
257284
return result;
258285
}
259286

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,44 @@ describe('parsers', () => {
144144
expect(parseNpmLikeManifest(stdout)).toEqual({ name: 'foo', version: '1.1.0' });
145145
});
146146

147+
it('should skip invalid manifests in an array', () => {
148+
const stdout = JSON.stringify([
149+
{ name: 'foo', version: '1.0.0' },
150+
{ name: 'foo' }, // Missing version
151+
{ version: '1.2.0' }, // Missing name
152+
null,
153+
"invalid",
154+
]);
155+
expect(parseNpmLikeManifest(stdout)).toEqual({ name: 'foo', version: '1.0.0' });
156+
});
157+
158+
it('should return null if no valid manifests found in the array', () => {
159+
const stdout = JSON.stringify([
160+
{ name: 'foo' },
161+
{ version: '1.2.0' },
162+
]);
163+
expect(parseNpmLikeManifest(stdout)).toBeNull();
164+
});
165+
166+
it('should return null for invalid single object', () => {
167+
const stdout = JSON.stringify({ name: 'foo' }); // Missing version
168+
expect(parseNpmLikeManifest(stdout)).toBeNull();
169+
});
170+
171+
it('should skip manifests with invalid semver versions in an array', () => {
172+
const stdout = JSON.stringify([
173+
{ name: 'foo', version: '1.0.0' },
174+
{ name: 'foo', version: 'invalid-version' },
175+
{ name: 'foo', version: '1.0' },
176+
]);
177+
expect(parseNpmLikeManifest(stdout)).toEqual({ name: 'foo', version: '1.0.0' });
178+
});
179+
180+
it('should return null for single object with invalid semver version', () => {
181+
const stdout = JSON.stringify({ name: 'foo', version: 'invalid-version' });
182+
expect(parseNpmLikeManifest(stdout)).toBeNull();
183+
});
184+
147185
it('should return null for empty stdout', () => {
148186
expect(parseNpmLikeManifest('')).toBeNull();
149187
});

0 commit comments

Comments
 (0)