/
githubmirror
/
angular-cli
Обзор
Документация
Войти
/
githubmirror
/
angular-cli
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
packages/angular/cli/src/package-managers/parsers_spec.ts
643 строки
21 KB
Alan Agius
fix(@angular/cli): support npm 12 metadata array and error formats
30 июл 2026, 10:46
30 июл 2026, 10:46
e148a8d
Код
Авторство
О чём код?
/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ import { parseBunDependencies, parseNpmBeforeDate, parseNpmLikeDependencies, parseNpmLikeError, parseNpmLikeManifest, parseNpmLikeMetadata, parsePnpmReleaseAge, parseYarnClassicDependencies, parseYarnClassicError, parseYarnClassicManifest, parseYarnModernDependencies, parseYarnReleaseAge, } from './parsers'; describe('parsers', () => { describe('parseNpmLikeDependencies', () => { it('should parse simple dependencies', () => { const stdout = JSON.stringify({ dependencies: { rxjs: { version: '7.8.2', }, }, }); const deps = parseNpmLikeDependencies(stdout); expect(deps.size).toBe(1); expect(deps.get('rxjs')).toEqual({ name: 'rxjs', version: '7.8.2', path: undefined }); }); it('should parse dependencies from current subproject and hoisted root', () => { const stdout = JSON.stringify({ version: '1.0.0', name: 'monorepo-root', dependencies: { app: { version: '1.0.0', resolved: 'file:../packages/app', dependencies: { rxjs: { version: '7.8.1', }, }, }, typescript: { version: '5.9.3', }, }, }); const deps = parseNpmLikeDependencies(stdout, undefined, { workspacePackageName: 'app' }); expect(deps.size).toBe(2); expect(deps.get('rxjs')).toEqual({ name: 'rxjs', version: '7.8.1', path: undefined }); expect(deps.get('typescript')).toEqual({ name: 'typescript', version: '5.9.3', path: undefined, }); }); it('should parse dependencies from array when workspacePackageName matches a project', () => { const stdout = JSON.stringify([ { name: 'monorepo-root', version: '1.0.0', path: '/workspace', dependencies: { typescript: { version: '5.9.3', }, }, }, { name: 'app', version: '1.0.0', path: '/workspace/apps/test-app', dependencies: { rxjs: { version: '7.8.2', }, }, }, ]); const deps = parseNpmLikeDependencies(stdout, undefined, { workspacePackageName: 'app' }); expect(deps.size).toBe(2); expect(deps.get('rxjs')).toEqual({ name: 'rxjs', version: '7.8.2', path: undefined }); expect(deps.get('typescript')).toEqual({ name: 'typescript', version: '5.9.3', path: undefined, }); }); it('should parse dependencies from array when workspacePackageName matches the root project', () => { const stdout = JSON.stringify([ { name: 'monorepo-root', version: '1.0.0', path: '/workspace', dependencies: { typescript: { version: '5.9.3', }, }, }, { name: 'other-app', version: '1.0.0', path: '/workspace/apps/other-app', dependencies: { rxjs: { version: '7.8.2', }, }, }, ]); const deps = parseNpmLikeDependencies(stdout, undefined, { workspacePackageName: 'monorepo-root', }); expect(deps.size).toBe(1); expect(deps.get('typescript')).toEqual({ name: 'typescript', version: '5.9.3', path: undefined, }); }); it('should parse nested dependencies and supplement with hoisted root dependencies in npm workspaces', () => { const stdout = JSON.stringify({ name: 'monorepo-root', version: '1.0.0', dependencies: { app: { version: '1.0.0', resolved: 'file:apps/test-app', dependencies: { rxjs: { version: '7.8.2', }, }, }, typescript: { version: '5.9.3', }, }, }); const deps = parseNpmLikeDependencies(stdout, undefined, { workspacePackageName: 'app', }); expect(deps.size).toBe(2); expect(deps.get('rxjs')).toEqual({ name: 'rxjs', version: '7.8.2', path: undefined }); expect(deps.get('typescript')).toEqual({ name: 'typescript', version: '5.9.3', path: undefined, }); }); it('should return empty map and not throw on empty array or non-object json', () => { expect(parseNpmLikeDependencies('[]').size).toBe(0); expect(parseNpmLikeDependencies('null').size).toBe(0); expect(parseNpmLikeDependencies('"not-an-object"').size).toBe(0); }); }); describe('parseNpmLikeError', () => { it('should parse a structured JSON error from modern yarn', () => { const stdout = JSON.stringify({ code: 'ERR_PNPM_NO_SUCH_PACKAGE', summary: 'No such package.', detail: 'Package not found.', }); const error = parseNpmLikeError(stdout); expect(error).toEqual({ code: 'ERR_PNPM_NO_SUCH_PACKAGE', summary: 'No such package.', detail: 'Package not found.', }); }); it('should parse a plain text error from npm', () => { const stdout = 'npm error code E404\nnpm error 404 Not Found - GET https://registry.example.com/non-existent-package'; const error = parseNpmLikeError(stdout); expect(error).toEqual({ code: 'E404', summary: '404 Not Found - GET https://registry.example.com/non-existent-package', }); }); it('should parse a plain text error from npm with ERR!', () => { const stderr = 'npm ERR! code E404\nnpm ERR! 404 Not Found - GET https://registry.example.com/non-existent-package'; const error = parseNpmLikeError(stderr); expect(error).toEqual({ code: 'E404', summary: '404 Not Found - GET https://registry.example.com/non-existent-package', }); }); it('should parse a structured JSON error with a message property', () => { const stderr = JSON.stringify({ code: 'EUNSUPPORTEDPROTOCOL', message: 'Unsupported protocol.', detail: 'The protocol "invalid:" is not supported.', }); const error = parseNpmLikeError(stderr); expect(error).toEqual({ code: 'EUNSUPPORTEDPROTOCOL', summary: 'Unsupported protocol.', detail: 'The protocol "invalid:" is not supported.', }); }); it('should return null for empty stdout', () => { const error = parseNpmLikeError(''); expect(error).toBeNull(); }); it('should return null for unparsable stdout', () => { 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.', }); }); 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', () => { it('should parse a single manifest', () => { const stdout = JSON.stringify({ name: 'foo', version: '1.0.0' }); expect(parseNpmLikeManifest(stdout)).toEqual({ name: 'foo', version: '1.0.0' }); }); it('should return the last manifest from an array', () => { const stdout = JSON.stringify([ { name: 'foo', version: '1.0.0' }, { name: 'foo', version: '1.1.0' }, ]); expect(parseNpmLikeManifest(stdout)).toEqual({ name: 'foo', version: '1.1.0' }); }); it('should return the highest version manifest from an unsorted array', () => { const stdout = JSON.stringify([ { name: 'foo', version: '1.1.0' }, { name: 'foo', version: '1.0.0' }, ]); expect(parseNpmLikeManifest(stdout)).toEqual({ name: 'foo', version: '1.1.0' }); }); it('should skip invalid manifests in an array', () => { const stdout = JSON.stringify([ { name: 'foo', version: '1.0.0' }, { name: 'foo' }, // Missing version { version: '1.2.0' }, // Missing name null, 'invalid', ]); expect(parseNpmLikeManifest(stdout)).toEqual({ name: 'foo', version: '1.0.0' }); }); it('should return null if no valid manifests found in the array', () => { const stdout = JSON.stringify([{ name: 'foo' }, { version: '1.2.0' }]); expect(parseNpmLikeManifest(stdout)).toBeNull(); }); it('should return null for invalid single object', () => { const stdout = JSON.stringify({ name: 'foo' }); // Missing version expect(parseNpmLikeManifest(stdout)).toBeNull(); }); it('should skip manifests with invalid semver versions in an array', () => { const stdout = JSON.stringify([ { name: 'foo', version: '1.0.0' }, { name: 'foo', version: 'invalid-version' }, { name: 'foo', version: '1.0' }, ]); expect(parseNpmLikeManifest(stdout)).toEqual({ name: 'foo', version: '1.0.0' }); }); it('should return null for single object with invalid semver version', () => { const stdout = JSON.stringify({ name: 'foo', version: 'invalid-version' }); expect(parseNpmLikeManifest(stdout)).toBeNull(); }); it('should return null for empty stdout', () => { expect(parseNpmLikeManifest('')).toBeNull(); }); }); 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({ type: 'inspect', data: { name: 'foo', version: '1.0.0' }, }); expect(parseYarnClassicManifest(stdout)).toEqual({ name: 'foo', version: '1.0.0' }); }); it('should return null for invalid manifest', () => { const stdout = JSON.stringify({ type: 'inspect', data: { name: 'foo' }, }); expect(parseYarnClassicManifest(stdout)).toBeNull(); }); it('should return null if no inspect type found', () => { const stdout = JSON.stringify({ type: 'other', data: { name: 'foo', version: '1.0.0' }, }); expect(parseYarnClassicManifest(stdout)).toBeNull(); }); }); describe('parseYarnClassicError', () => { it('should parse a 404 from verbose logs', () => { const stdout = '{"type":"verbose","data":"Request "https://registry.example.com/non-existent-package" finished with status code 404."}'; const error = parseYarnClassicError(stdout); expect(error).toEqual({ code: 'E404', summary: 'Request failed with status code 404.', }); }); it('should parse a non-404 HTTP error from verbose logs', () => { const stdout = '{"type":"verbose","data":"Request "https://registry.example.com/private-package" finished with status code 401."}'; const error = parseYarnClassicError(stdout); expect(error).toEqual({ code: 'E401', summary: 'Request failed with status code 401.', }); }); it('should parse a generic JSON error when no HTTP status is found', () => { const stdout = '{"type":"error","data":"An unexpected error occurred."}'; const error = parseYarnClassicError(stdout); expect(error).toEqual({ code: 'UNKNOWN_ERROR', summary: 'An unexpected error occurred.', }); }); it('should return null for empty stdout', () => { const error = parseYarnClassicError(''); expect(error).toBeNull(); }); it('should return null for unparsable stdout', () => { const error = parseYarnClassicError('A random error message.'); expect(error).toBeNull(); }); }); describe('parseYarnClassicDependencies', () => { it('should parse yarn classic list output', () => { const stdout = JSON.stringify({ type: 'tree', data: { trees: [{ name: 'rxjs@7.8.2', children: [] }], }, }); const deps = parseYarnClassicDependencies(stdout); expect(deps.size).toBe(1); expect(deps.get('rxjs')).toEqual({ name: 'rxjs', version: '7.8.2' }); }); it('should handle scoped packages', () => { const stdout = JSON.stringify({ type: 'tree', data: { trees: [{ name: '@angular/core@18.0.0', children: [] }], }, }); const deps = parseYarnClassicDependencies(stdout); expect(deps.size).toBe(1); expect(deps.get('@angular/core')).toEqual({ name: '@angular/core', version: '18.0.0' }); }); it('should return empty map for empty stdout', () => { expect(parseYarnClassicDependencies('').size).toBe(0); }); }); describe('parseBunDependencies', () => { it('should parse bun pm ls output', () => { const stdout = ` /tmp/angular-cli-e2e-PiL5n3/e2e-test/assets/19.0-project-1767113081927 node_modules (1084) ├── @angular-devkit/build-angular@20.3.13 ├── @angular/cli@20.3.13 ├── jasmine-core @5.6.0 ├── rxjs @7.8.2 └── zone.js @0.15.1 `.trim(); const deps = parseBunDependencies(stdout); expect(deps.size).toBe(5); expect(deps.get('@angular-devkit/build-angular')).toEqual({ name: '@angular-devkit/build-angular', version: '20.3.13', }); expect(deps.get('@angular/cli')).toEqual({ name: '@angular/cli', version: '20.3.13' }); expect(deps.get('jasmine-core')).toEqual({ name: 'jasmine-core', version: '5.6.0' }); expect(deps.get('rxjs')).toEqual({ name: 'rxjs', version: '7.8.2' }); expect(deps.get('zone.js')).toEqual({ name: 'zone.js', version: '0.15.1' }); }); it('should return empty map for empty stdout', () => { expect(parseBunDependencies('').size).toBe(0); }); it('should skip lines that do not match the pattern', () => { const stdout = ` project node_modules ├── invalid-line └── another-invalid `.trim(); expect(parseBunDependencies(stdout).size).toBe(0); }); }); describe('parseYarnModernDependencies', () => { it('should parse yarn info --name-only --json output', () => { const stdout = ` "karma@npm:6.4.4" "rxjs@npm:7.8.2" "tslib@npm:2.8.1" "typescript@patch:typescript@npm%3A5.9.3#optional!builtin<compat/typescript>::version=5.9.3&hash=5786d5" `.trim(); const deps = parseYarnModernDependencies(stdout); expect(deps.size).toBe(4); expect(deps.get('karma')).toEqual({ name: 'karma', version: '6.4.4' }); expect(deps.get('rxjs')).toEqual({ name: 'rxjs', version: '7.8.2' }); expect(deps.get('tslib')).toEqual({ name: 'tslib', version: '2.8.1' }); expect(deps.get('typescript')).toEqual({ name: 'typescript', version: '5.9.3', }); }); it('should handle scoped packages', () => { const stdout = '"@angular/core@npm:20.3.15"'; const deps = parseYarnModernDependencies(stdout); expect(deps.get('@angular/core')).toEqual({ name: '@angular/core', version: '20.3.15', }); }); it('should return empty map for empty stdout', () => { expect(parseYarnModernDependencies('').size).toBe(0); }); }); describe('parsePnpmReleaseAge', () => { it('should parse minutes correctly', () => { expect(parsePnpmReleaseAge('1440', '10.15.0')).toBe(1440 * 60000); expect(parsePnpmReleaseAge(' 10 ', '10.15.0')).toBe(10 * 60000); }); it('should return 0 for undefined/null/empty values when version is < 11', () => { expect(parsePnpmReleaseAge('', '10.15.0')).toBe(0); expect(parsePnpmReleaseAge('undefined', '10.15.0')).toBe(0); expect(parsePnpmReleaseAge('null', '10.15.0')).toBe(0); }); it('should return 1440 minutes for undefined/null/empty values when version is >= 11', () => { expect(parsePnpmReleaseAge('', '11.2.0')).toBe(1440 * 60000); expect(parsePnpmReleaseAge('undefined', '11.2.0')).toBe(1440 * 60000); expect(parsePnpmReleaseAge('null', '11.2.0')).toBe(1440 * 60000); }); it('should return 0 for invalid inputs', () => { expect(parsePnpmReleaseAge('abc', '10.15.0')).toBe(0); }); }); describe('parseYarnReleaseAge', () => { it('should parse duration units correctly', () => { expect(parseYarnReleaseAge('3d', '3.6.0')).toBe(3 * 86400000); expect(parseYarnReleaseAge('24h', '3.6.0')).toBe(24 * 3600000); expect(parseYarnReleaseAge('30m', '3.6.0')).toBe(30 * 60000); expect(parseYarnReleaseAge('60s', '3.6.0')).toBe(60000); expect(parseYarnReleaseAge('1w', '3.6.0')).toBe(604800000); }); it('should parse raw numbers as minutes', () => { expect(parseYarnReleaseAge('3', '3.6.0')).toBe(3 * 60000); expect(parseYarnReleaseAge('60', '3.6.0')).toBe(60 * 60000); }); it('should return 0 for undefined/null/empty values when version is < 4', () => { expect(parseYarnReleaseAge('', '3.6.0')).toBe(0); expect(parseYarnReleaseAge('undefined', '3.6.0')).toBe(0); expect(parseYarnReleaseAge('null', '3.6.0')).toBe(0); }); it('should return 1440 minutes for undefined/null/empty values when version is >= 4', () => { expect(parseYarnReleaseAge('', '4.1.0')).toBe(1440 * 60000); expect(parseYarnReleaseAge('undefined', '4.1.0')).toBe(1440 * 60000); expect(parseYarnReleaseAge('null', '4.1.0')).toBe(1440 * 60000); }); }); describe('parseNpmBeforeDate', () => { it('should parse date string and convert to age limit', () => { const tenDaysAgo = new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString(); const age = parseNpmBeforeDate(tenDaysAgo); expect(age).toBeGreaterThanOrEqual(10 * 24 * 60 * 60 * 1000 - 1000); expect(age).toBeLessThanOrEqual(10 * 24 * 60 * 60 * 1000 + 1000); }); it('should return 0 for undefined/null/empty values', () => { expect(parseNpmBeforeDate('')).toBe(0); expect(parseNpmBeforeDate('undefined')).toBe(0); expect(parseNpmBeforeDate('null')).toBe(0); }); }); });