diff --git a/packages/angular/cli/src/package-managers/package-manager-descriptor.ts b/packages/angular/cli/src/package-managers/package-manager-descriptor.ts index 2dfe75ee01cd..d99d2c950702 100644 --- a/packages/angular/cli/src/package-managers/package-manager-descriptor.ts +++ b/packages/angular/cli/src/package-managers/package-manager-descriptor.ts @@ -99,6 +99,15 @@ export interface PackageManagerDescriptor { /** A function that formats the arguments for field-filtered registry views. */ readonly viewCommandFieldArgFormatter?: (fields: readonly string[]) => string[]; + /** An optional custom function to fetch registry metadata when the default logic is not sufficient. */ + readonly getRegistryMetadata?: ( + packageName: string, + fetchAndParse: ( + args: readonly string[], + parser: (stdout: string, logger?: Logger) => T | null, + ) => Promise, + ) => Promise; + /** A collection of functions to parse the output of specific commands. */ readonly outputParsers: { /** A function to parse the output of `listDependenciesCommand`. */ @@ -273,6 +282,38 @@ export const SUPPORTED_PACKAGE_MANAGERS = { versionCommand: ['--version'], listDependenciesCommand: ['pm', 'ls'], getManifestCommand: ['pm', 'view', '--json'], + getRegistryMetadata: async (packageName, fetchAndParse) => { + const [distTags, versions] = await Promise.all([ + fetchAndParse(['pm', 'view', '--json', packageName, 'dist-tags'], (stdout) => { + if (!stdout) { + return {}; + } + + const parsed = JSON.parse(stdout); + + return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}; + }), + fetchAndParse(['pm', 'view', '--json', packageName, 'versions'], (stdout) => { + if (!stdout) { + return null; + } + + const parsed = JSON.parse(stdout); + + return Array.isArray(parsed) ? parsed : [parsed]; + }), + ]); + + if (!versions || versions.length === 0) { + return null; + } + + return { + name: packageName, + 'dist-tags': (distTags || {}) as Record, + versions: versions as string[], + }; + }, outputParsers: { listDependencies: parseBunDependencies, getRegistryManifest: parseNpmLikeManifest, diff --git a/packages/angular/cli/src/package-managers/package-manager.ts b/packages/angular/cli/src/package-managers/package-manager.ts index 46adbd118b9b..a5ebfad62553 100644 --- a/packages/angular/cli/src/package-managers/package-manager.ts +++ b/packages/angular/cli/src/package-managers/package-manager.ts @@ -441,19 +441,37 @@ export class PackageManager { packageName: string, options: { timeout?: number; registry?: string; bypassCache?: boolean } = {}, ): Promise { - const commandArgs = [...this.descriptor.getManifestCommand, packageName]; - const formatter = this.descriptor.viewCommandFieldArgFormatter; - if (formatter) { - commandArgs.push(...formatter(METADATA_FIELDS)); + const cacheKey = options.registry ? `${packageName}|${options.registry}` : packageName; + + if (!options.bypassCache) { + const cached = this.#metadataCache.get(cacheKey); + if (cached !== undefined) { + return cached; + } } - const cacheKey = options.registry ? `${packageName}|${options.registry}` : packageName; + let metadata: PackageMetadata | null; + if (this.descriptor.getRegistryMetadata) { + metadata = await this.descriptor.getRegistryMetadata(packageName, (args, parser) => + this.#fetchAndParse(args, parser, options), + ); + } else { + const commandArgs = [...this.descriptor.getManifestCommand, packageName]; + const formatter = this.descriptor.viewCommandFieldArgFormatter; + if (formatter) { + commandArgs.push(...formatter(METADATA_FIELDS)); + } - return this.#fetchAndParse( - commandArgs, - (stdout, logger) => this.descriptor.outputParsers.getRegistryMetadata(stdout, logger), - { ...options, cache: this.#metadataCache, cacheKey }, - ); + metadata = await this.#fetchAndParse( + commandArgs, + (stdout, logger) => this.descriptor.outputParsers.getRegistryMetadata(stdout, logger), + options, + ); + } + + this.#metadataCache.set(cacheKey, metadata); + + return metadata; } /** diff --git a/packages/angular/cli/src/package-managers/package-manager_spec.ts b/packages/angular/cli/src/package-managers/package-manager_spec.ts index 176b604eeca6..105fcf5930b0 100644 --- a/packages/angular/cli/src/package-managers/package-manager_spec.ts +++ b/packages/angular/cli/src/package-managers/package-manager_spec.ts @@ -195,6 +195,42 @@ describe('PackageManager', () => { }); }); + 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');