/
githubmirror
/
angular-cli
Обзор
Документация
Войти
/
githubmirror
/
angular-cli
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
packages/angular/cli/src/package-managers/package-manager_spec.ts
589 строк
23 KB
Charles Lyding
fix(@angular/cli): copy packageManager field and yarn config for temp installs
13 июл 2026, 14:20
13 июл 2026, 14:20
3789cee
Код
Авторство
О чём код?
/** * @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 { MANIFEST_FIELDS, PackageManager } from './package-manager'; import { SUPPORTED_PACKAGE_MANAGERS } from './package-manager-descriptor'; import { MockHost } from './testing/mock-host'; describe('PackageManager', () => { let host: MockHost; let runCommandSpy: jasmine.Spy; const descriptor = SUPPORTED_PACKAGE_MANAGERS['npm']; beforeEach(() => { host = new MockHost(); runCommandSpy = spyOn(host, 'runCommand').and.resolveTo({ stdout: '1.2.3', stderr: '' }); }); describe('getRegistryManifest', () => { it('should quote complex range specifiers when required by the host', async () => { // Simulate a quoting host Object.assign(host, { requiresQuoting: true }); const pm = new PackageManager(host, '/tmp', descriptor); const manifest = { name: 'foo', version: '1.0.0' }; runCommandSpy.and.resolveTo({ stdout: JSON.stringify(manifest), stderr: '' }); await pm.getRegistryManifest('foo', '>=1.0.0 <2.0.0'); expect(runCommandSpy).toHaveBeenCalledWith( descriptor.binary, [...descriptor.getManifestCommand, '"foo@>=1.0.0 <2.0.0"', ...MANIFEST_FIELDS], jasmine.anything(), ); }); it('should NOT quote complex range specifiers when not required by the host', async () => { // Simulate a non-quoting host Object.assign(host, { requiresQuoting: false }); const pm = new PackageManager(host, '/tmp', descriptor); const manifest = { name: 'foo', version: '1.0.0' }; runCommandSpy.and.resolveTo({ stdout: JSON.stringify(manifest), stderr: '' }); await pm.getRegistryManifest('foo', '>=1.0.0 <2.0.0'); expect(runCommandSpy).toHaveBeenCalledWith( descriptor.binary, [...descriptor.getManifestCommand, 'foo@>=1.0.0 <2.0.0', ...MANIFEST_FIELDS], jasmine.anything(), ); }); }); describe('getVersion', () => { it('should fetch the version from the package manager if not cached', async () => { const pm = new PackageManager(host, '/tmp', descriptor); const version = await pm.getVersion(); expect(version).toBe('1.2.3'); expect(runCommandSpy).toHaveBeenCalledWith( descriptor.binary, descriptor.versionCommand, jasmine.objectContaining({ cwd: '/tmp' }), ); }); it('should cache the version after the first fetch', async () => { const pm = new PackageManager(host, '/tmp', descriptor); await pm.getVersion(); await pm.getVersion(); expect(runCommandSpy).toHaveBeenCalledTimes(1); }); it('should use the version provided in the constructor', async () => { const pm = new PackageManager(host, '/tmp', descriptor, { version: '4.5.6' }); const version = await pm.getVersion(); expect(version).toBe('4.5.6'); expect(runCommandSpy).not.toHaveBeenCalled(); }); }); describe('acquireTempPackage', () => { it('should copy and sanitize pnpm-workspace.yaml when package manager is pnpm and workspace file exists', async () => { const pnpmDescriptor = SUPPORTED_PACKAGE_MANAGERS['pnpm']; const testHost = new MockHost({ '/tmp/project/node_modules': true }); const pm = new PackageManager(testHost, '/tmp/project', pnpmDescriptor); const createTempDirectorySpy = spyOn(testHost, 'createTempDirectory').and.resolveTo( '/tmp/project/node_modules/angular-cli-tmp-packages-abc', ); const mockWorkspaceContent = [ 'packages:', ' - .', ' - packages/*', 'minimumReleaseAge: 1440', 'minimumReleaseAgeExclude:', " - '@angular/*'", 'overrides:', " '@angular/build': workspace:*", 'packageExtensions:', ' vitest:', ' peerDependencies:', " '@vitest/coverage-v8': '*'", ].join('\n'); const readFileSpy = spyOn(testHost, 'readFile').and.resolveTo(mockWorkspaceContent); const writeFileSpy = spyOn(testHost, 'writeFile').and.resolveTo(); spyOn(testHost, 'runCommand').and.resolveTo({ stdout: '', stderr: '' }); const { workingDirectory } = await pm.acquireTempPackage('foo@1.0.0'); expect(workingDirectory).toBe('/tmp/project/node_modules/angular-cli-tmp-packages-abc'); expect(createTempDirectorySpy).toHaveBeenCalledWith('/tmp/project/node_modules'); expect(readFileSpy).toHaveBeenCalledWith('/tmp/project/pnpm-workspace.yaml'); const expectedSanitizedContent = [ 'packages:', " - '.'", 'minimumReleaseAge: 1440', 'minimumReleaseAgeExclude:', " - '@angular/*'", 'packageExtensions:', ' vitest:', ' peerDependencies:', " '@vitest/coverage-v8': '*'", ].join('\n'); expect(writeFileSpy).toHaveBeenCalledWith( '/tmp/project/node_modules/angular-cli-tmp-packages-abc/pnpm-workspace.yaml', expectedSanitizedContent, ); expect(writeFileSpy).toHaveBeenCalledWith( '/tmp/project/node_modules/angular-cli-tmp-packages-abc/package.json', '{}', ); }); it('should write empty pnpm-workspace.yaml as fallback when package manager is pnpm and workspace file does not exist', async () => { const pnpmDescriptor = SUPPORTED_PACKAGE_MANAGERS['pnpm']; const testHost = new MockHost({ '/tmp/project/node_modules': true }); const pm = new PackageManager(testHost, '/tmp/project', pnpmDescriptor); const createTempDirectorySpy = spyOn(testHost, 'createTempDirectory').and.resolveTo( '/tmp/project/node_modules/angular-cli-tmp-packages-abc', ); const readFileSpy = spyOn(testHost, 'readFile').and.throwError( new Error('ENOENT: no such file or directory'), ); const writeFileSpy = spyOn(testHost, 'writeFile').and.resolveTo(); spyOn(testHost, 'runCommand').and.resolveTo({ stdout: '', stderr: '' }); const { workingDirectory } = await pm.acquireTempPackage('foo@1.0.0'); expect(workingDirectory).toBe('/tmp/project/node_modules/angular-cli-tmp-packages-abc'); expect(createTempDirectorySpy).toHaveBeenCalledWith('/tmp/project/node_modules'); expect(readFileSpy).toHaveBeenCalledWith('/tmp/project/pnpm-workspace.yaml'); expect(writeFileSpy).toHaveBeenCalledWith( '/tmp/project/node_modules/angular-cli-tmp-packages-abc/package.json', '{}', ); expect(writeFileSpy).toHaveBeenCalledWith( '/tmp/project/node_modules/angular-cli-tmp-packages-abc/pnpm-workspace.yaml', "packages:\n - '.'\n", ); }); it('should NOT write pnpm-workspace.yaml when package manager is npm', async () => { const npmDescriptor = SUPPORTED_PACKAGE_MANAGERS['npm']; const testHost = new MockHost({ '/tmp/project/node_modules': true }); const pm = new PackageManager(testHost, '/tmp/project', npmDescriptor); const createTempDirectorySpy = spyOn(testHost, 'createTempDirectory').and.resolveTo( '/tmp/project/node_modules/angular-cli-tmp-packages-abc', ); const writeFileSpy = spyOn(testHost, 'writeFile').and.resolveTo(); spyOn(testHost, 'runCommand').and.resolveTo({ stdout: '', stderr: '' }); const { workingDirectory } = await pm.acquireTempPackage('foo@1.0.0'); expect(workingDirectory).toBe('/tmp/project/node_modules/angular-cli-tmp-packages-abc'); expect(createTempDirectorySpy).toHaveBeenCalledWith('/tmp/project/node_modules'); expect(writeFileSpy).toHaveBeenCalledWith( '/tmp/project/node_modules/angular-cli-tmp-packages-abc/package.json', '{}', ); expect(writeFileSpy).not.toHaveBeenCalledWith( '/tmp/project/node_modules/angular-cli-tmp-packages-abc/pnpm-workspace.yaml', '', ); }); it('should copy and sanitize .yarnrc.yml when package manager is yarn and it exists', async () => { const yarnDescriptor = SUPPORTED_PACKAGE_MANAGERS['yarn']; const testHost = new MockHost({ '/tmp/project/node_modules': true, '/tmp/project/.yarnrc.yml': [], }); const pm = new PackageManager(testHost, '/tmp/project', yarnDescriptor); const createTempDirectorySpy = spyOn(testHost, 'createTempDirectory').and.resolveTo( '/tmp/project/node_modules/angular-cli-tmp-packages-abc', ); const writeFileSpy = spyOn(testHost, 'writeFile').and.resolveTo(); const mockYarnRcContent = [ 'yarnPath: .yarn/releases/yarn-4.4.1.cjs', 'nodeLinker: pnp', 'customOption:', ' nodeLinker: pnp-nested', ' plugins:', ' - nested-plugin', 'plugins:', ' - path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs', ' spec: "@yarnpkg/plugin-typescript"', 'npmRegistryServer: "https://registry.npmjs.org"', ].join('\n'); spyOn(testHost, 'readFile').and.callFake(async (filePath) => { if (filePath.replace(/\\/g, '/').endsWith('.yarnrc.yml')) { return mockYarnRcContent; } if (filePath.replace(/\\/g, '/').endsWith('package.json')) { return JSON.stringify({ packageManager: 'yarn@4.4.1' }); } throw new Error(`Unexpected readFile call for ${filePath}`); }); spyOn(testHost, 'runCommand').and.resolveTo({ stdout: '4.4.1', stderr: '' }); const { workingDirectory } = await pm.acquireTempPackage('foo@1.0.0'); expect(workingDirectory).toBe('/tmp/project/node_modules/angular-cli-tmp-packages-abc'); expect(createTempDirectorySpy).toHaveBeenCalledWith('/tmp/project/node_modules'); const expectedYarnRcContent = [ 'customOption:', ' nodeLinker: pnp-nested', ' plugins:', ' - nested-plugin', 'npmRegistryServer: "https://registry.npmjs.org"', 'nodeLinker: node-modules', ].join('\n'); expect(writeFileSpy).toHaveBeenCalledWith( '/tmp/project/node_modules/angular-cli-tmp-packages-abc/.yarnrc.yml', expectedYarnRcContent, ); expect(writeFileSpy).toHaveBeenCalledWith( '/tmp/project/node_modules/angular-cli-tmp-packages-abc/package.json', JSON.stringify({ packageManager: 'yarn@4.4.1' }, null, 2), ); }); it('should copy packageManager field to temp package.json if version is resolved', async () => { const npmDescriptor = SUPPORTED_PACKAGE_MANAGERS['npm']; const testHost = new MockHost({ '/tmp/project/node_modules': true, }); const pm = new PackageManager(testHost, '/tmp/project', npmDescriptor); spyOn(testHost, 'createTempDirectory').and.resolveTo( '/tmp/project/node_modules/angular-cli-tmp-packages-abc', ); const writeFileSpy = spyOn(testHost, 'writeFile').and.resolveTo(); spyOn(testHost, 'runCommand').and.resolveTo({ stdout: '10.32.1', stderr: '' }); await pm.acquireTempPackage('foo@1.0.0'); expect(writeFileSpy).toHaveBeenCalledWith( '/tmp/project/node_modules/angular-cli-tmp-packages-abc/package.json', JSON.stringify({ packageManager: 'npm@10.32.1' }, null, 2), ); }); it('should write empty package.json if package manager version cannot be resolved', async () => { const npmDescriptor = SUPPORTED_PACKAGE_MANAGERS['npm']; const testHost = new MockHost({ '/tmp/project/node_modules': true }); const pm = new PackageManager(testHost, '/tmp/project', npmDescriptor); spyOn(testHost, 'createTempDirectory').and.resolveTo( '/tmp/project/node_modules/angular-cli-tmp-packages-abc', ); const writeFileSpy = spyOn(testHost, 'writeFile').and.resolveTo(); spyOn(testHost, 'runCommand').and.resolveTo({ stdout: '', stderr: '' }); await pm.acquireTempPackage('foo@1.0.0'); expect(writeFileSpy).toHaveBeenCalledWith( '/tmp/project/node_modules/angular-cli-tmp-packages-abc/package.json', '{}', ); }); }); describe('getRegistryMetadata', () => { it('should query dist-tags and versions separately for bun', async () => { const bunDescriptor = SUPPORTED_PACKAGE_MANAGERS['bun']; const pm = new PackageManager(host, '/tmp', bunDescriptor); runCommandSpy.and.callFake((binary, args) => { if (args.includes('dist-tags')) { return Promise.resolve({ stdout: JSON.stringify({ latest: '2.0.0' }), stderr: '' }); } else if (args.includes('versions')) { return Promise.resolve({ stdout: JSON.stringify(['1.0.0', '2.0.0']), stderr: '' }); } return Promise.resolve({ stdout: '', stderr: '' }); }); const metadata = await pm.getRegistryMetadata('foo'); expect(metadata).toEqual({ name: 'foo', 'dist-tags': { latest: '2.0.0' }, versions: ['1.0.0', '2.0.0'], }); expect(runCommandSpy).toHaveBeenCalledWith( 'bun', ['pm', 'view', '--json', 'foo', 'dist-tags'], jasmine.anything(), ); expect(runCommandSpy).toHaveBeenCalledWith( 'bun', ['pm', 'view', '--json', 'foo', 'versions'], jasmine.anything(), ); }); }); describe('initializationError', () => { it('should throw initializationError when running commands', async () => { const error = new Error('Not installed'); const pm = new PackageManager(host, '/tmp', descriptor, { initializationError: error }); expect(() => pm.ensureInstalled()).toThrow(error); await expectAsync(pm.getVersion()).toBeRejectedWith(error); await expectAsync(pm.install()).toBeRejectedWith(error); await expectAsync(pm.add('foo', 'none', false, false, false)).toBeRejectedWith(error); }); it('should not throw initializationError for operations that do not require the binary', async () => { const error = new Error('Not installed'); const pm = new PackageManager(host, '/tmp', descriptor, { initializationError: error }); // Mock readFile for getManifest directory case spyOn(host, 'readFile').and.resolveTo('{"name": "foo", "version": "1.0.0"}'); // Should not throw const manifest = await pm.getManifest({ type: 'directory', fetchSpec: '/tmp/foo', // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); expect(manifest).toEqual({ name: 'foo', version: '1.0.0' }); }); }); describe('getMinimumReleaseAge', () => { it('should return 0 when the package manager descriptor does not support getReleaseAgeConfigCommand', async () => { const pm = new PackageManager(host, '/tmp', SUPPORTED_PACKAGE_MANAGERS['bun']); const age = await pm.getMinimumReleaseAge(); expect(age).toBe(0); }); it('should query release age configuration value and parse npm before date parameter correctly', async () => { const npmDescriptor = SUPPORTED_PACKAGE_MANAGERS['npm']; const pm = new PackageManager(host, '/tmp', npmDescriptor); const testDate = new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString(); runCommandSpy.and.callFake((binary: string, args: readonly string[]) => { if (args.includes('--version')) { return Promise.resolve({ stdout: '10.8.0', stderr: '' }); } if (args.includes('before')) { return Promise.resolve({ stdout: testDate, stderr: '' }); } return Promise.reject(new Error('Unknown command')); }); const age = await pm.getMinimumReleaseAge(); // Allow +/- 2000ms variance due to Date.now() timing difference between mock and execution. expect(age).toBeGreaterThanOrEqual(10 * 24 * 60 * 60 * 1000 - 2000); expect(age).toBeLessThanOrEqual(10 * 24 * 60 * 60 * 1000 + 2000); expect(runCommandSpy).toHaveBeenCalledWith( 'npm', ['config', 'get', 'before'], jasmine.anything(), ); }); it('should query release age configuration value and parse pnpm minutes value correctly', async () => { const pnpmDescriptor = SUPPORTED_PACKAGE_MANAGERS['pnpm']; const pm = new PackageManager(host, '/tmp', pnpmDescriptor); runCommandSpy.and.callFake((binary: string, args: readonly string[]) => { if (args.includes('--version')) { return Promise.resolve({ stdout: '10.15.0', stderr: '' }); } if (args.includes('minimum-release-age')) { return Promise.resolve({ stdout: '1440', stderr: '' }); } return Promise.reject(new Error('Unknown command')); }); const age = await pm.getMinimumReleaseAge(); expect(age).toBe(1440 * 60 * 1000); expect(runCommandSpy).toHaveBeenCalledWith( 'pnpm', ['config', 'get', 'minimum-release-age'], jasmine.anything(), ); }); it('should query release age configuration value and parse yarn duration values correctly', async () => { const yarnDescriptor = SUPPORTED_PACKAGE_MANAGERS['yarn']; const pm = new PackageManager(host, '/tmp', yarnDescriptor); runCommandSpy.and.callFake((binary: string, args: readonly string[]) => { if (args.includes('--version')) { return Promise.resolve({ stdout: '4.5.0', stderr: '' }); } if (args.includes('npmMinimalAgeGate')) { return Promise.resolve({ stdout: '3d', stderr: '' }); } return Promise.reject(new Error('Unknown command')); }); const age = await pm.getMinimumReleaseAge(); expect(age).toBe(3 * 24 * 60 * 60 * 1000); expect(runCommandSpy).toHaveBeenCalledWith( 'yarn', ['config', 'get', 'npmMinimalAgeGate'], jasmine.anything(), ); }); it('should query yarn configuration value and parse raw numbers as minutes', async () => { const yarnDescriptor = SUPPORTED_PACKAGE_MANAGERS['yarn']; const pm = new PackageManager(host, '/tmp', yarnDescriptor); runCommandSpy.and.callFake((binary: string, args: readonly string[]) => { if (args.includes('--version')) { return Promise.resolve({ stdout: '4.5.0', stderr: '' }); } if (args.includes('npmMinimalAgeGate')) { return Promise.resolve({ stdout: '3', stderr: '' }); } return Promise.reject(new Error('Unknown command')); }); const age = await pm.getMinimumReleaseAge(); expect(age).toBe(3 * 60 * 1000); }); it('should cache the release age after the first fetch', async () => { const pnpmDescriptor = SUPPORTED_PACKAGE_MANAGERS['pnpm']; const pm = new PackageManager(host, '/tmp', pnpmDescriptor); runCommandSpy.and.callFake((binary: string, args: readonly string[]) => { if (args.includes('--version')) { return Promise.resolve({ stdout: '10.15.0', stderr: '' }); } if (args.includes('minimum-release-age')) { return Promise.resolve({ stdout: '1440', stderr: '' }); } return Promise.reject(new Error('Unknown command')); }); const age1 = await pm.getMinimumReleaseAge(); const age2 = await pm.getMinimumReleaseAge(); expect(age1).toBe(1440 * 60 * 1000); expect(age2).toBe(1440 * 60 * 1000); expect(runCommandSpy).toHaveBeenCalledTimes(2); }); it('should default to 1440 minutes for pnpm v11+ when config is not specified', async () => { const pnpmDescriptor = SUPPORTED_PACKAGE_MANAGERS['pnpm']; const pm = new PackageManager(host, '/tmp', pnpmDescriptor); runCommandSpy.and.callFake((binary: string, args: readonly string[]) => { if (args.includes('minimum-release-age')) { return Promise.resolve({ stdout: 'undefined', stderr: '' }); } if (args.includes('--version')) { return Promise.resolve({ stdout: '11.2.0', stderr: '' }); } return Promise.reject(new Error('Unknown command')); }); const age = await pm.getMinimumReleaseAge(); expect(age).toBe(1440 * 60 * 1000); }); it('should default to 0 for pnpm v10 when config is not specified', async () => { const pnpmDescriptor = SUPPORTED_PACKAGE_MANAGERS['pnpm']; const pm = new PackageManager(host, '/tmp', pnpmDescriptor); runCommandSpy.and.callFake((binary: string, args: readonly string[]) => { if (args.includes('minimum-release-age')) { return Promise.resolve({ stdout: 'undefined', stderr: '' }); } if (args.includes('--version')) { return Promise.resolve({ stdout: '10.15.0', stderr: '' }); } return Promise.reject(new Error('Unknown command')); }); const age = await pm.getMinimumReleaseAge(); expect(age).toBe(0); }); it('should respect explicitly configured 0 value for pnpm v11+', async () => { const pnpmDescriptor = SUPPORTED_PACKAGE_MANAGERS['pnpm']; const pm = new PackageManager(host, '/tmp', pnpmDescriptor); runCommandSpy.and.callFake((binary: string, args: readonly string[]) => { if (args.includes('minimum-release-age')) { return Promise.resolve({ stdout: '0', stderr: '' }); } if (args.includes('--version')) { return Promise.resolve({ stdout: '11.2.0', stderr: '' }); } return Promise.reject(new Error('Unknown command')); }); const age = await pm.getMinimumReleaseAge(); expect(age).toBe(0); }); it('should default to 1440 minutes for yarn v4+ when config is not specified', async () => { const yarnDescriptor = SUPPORTED_PACKAGE_MANAGERS['yarn']; const pm = new PackageManager(host, '/tmp', yarnDescriptor); runCommandSpy.and.callFake((binary: string, args: readonly string[]) => { if (args.includes('npmMinimalAgeGate')) { return Promise.resolve({ stdout: 'undefined', stderr: '' }); } if (args.includes('--version')) { return Promise.resolve({ stdout: '4.1.0', stderr: '' }); } return Promise.reject(new Error('Unknown command')); }); const age = await pm.getMinimumReleaseAge(); expect(age).toBe(1440 * 60 * 1000); }); it('should default to 0 for yarn v3 when config is not specified', async () => { const yarnDescriptor = SUPPORTED_PACKAGE_MANAGERS['yarn']; const pm = new PackageManager(host, '/tmp', yarnDescriptor); runCommandSpy.and.callFake((binary: string, args: readonly string[]) => { if (args.includes('npmMinimalAgeGate')) { return Promise.resolve({ stdout: 'undefined', stderr: '' }); } if (args.includes('--version')) { return Promise.resolve({ stdout: '3.6.0', stderr: '' }); } return Promise.reject(new Error('Unknown command')); }); const age = await pm.getMinimumReleaseAge(); expect(age).toBe(0); }); it('should respect explicitly configured 0 value for yarn v4+', async () => { const yarnDescriptor = SUPPORTED_PACKAGE_MANAGERS['yarn']; const pm = new PackageManager(host, '/tmp', yarnDescriptor); runCommandSpy.and.callFake((binary: string, args: readonly string[]) => { if (args.includes('npmMinimalAgeGate')) { return Promise.resolve({ stdout: '0', stderr: '' }); } if (args.includes('--version')) { return Promise.resolve({ stdout: '4.1.0', stderr: '' }); } return Promise.reject(new Error('Unknown command')); }); const age = await pm.getMinimumReleaseAge(); expect(age).toBe(0); }); }); });