Skip to content

Commit 309a236

Browse files
committed
fix(@angular/cli): support registry metadata fetching under bun package manager
Bun's `pm view` command does not support requesting multiple fields at once (e.g. `pm view <pkg> dist-tags versions --json`), which is required by the default package manager abstraction to fetch package metadata during version compatibility search. This change introduces a custom `getRegistryMetadata` handler in the package manager descriptor, allowing individual package managers to override registry metadata fetching entirely. The `bun` descriptor now implements this by querying `dist-tags` and `versions` separately in parallel, and returning the aggregated metadata object.
1 parent 5875b60 commit 309a236

3 files changed

Lines changed: 105 additions & 10 deletions

File tree

packages/angular/cli/src/package-managers/package-manager-descriptor.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,15 @@ export interface PackageManagerDescriptor {
9999
/** A function that formats the arguments for field-filtered registry views. */
100100
readonly viewCommandFieldArgFormatter?: (fields: readonly string[]) => string[];
101101

102+
/** An optional custom function to fetch registry metadata when the default logic is not sufficient. */
103+
readonly getRegistryMetadata?: (
104+
packageName: string,
105+
fetchAndParse: <T>(
106+
args: readonly string[],
107+
parser: (stdout: string, logger?: Logger) => T | null,
108+
) => Promise<T | null>,
109+
) => Promise<PackageMetadata | null>;
110+
102111
/** A collection of functions to parse the output of specific commands. */
103112
readonly outputParsers: {
104113
/** A function to parse the output of `listDependenciesCommand`. */
@@ -273,6 +282,38 @@ export const SUPPORTED_PACKAGE_MANAGERS = {
273282
versionCommand: ['--version'],
274283
listDependenciesCommand: ['pm', 'ls'],
275284
getManifestCommand: ['pm', 'view', '--json'],
285+
getRegistryMetadata: async (packageName, fetchAndParse) => {
286+
const [distTags, versions] = await Promise.all([
287+
fetchAndParse(['pm', 'view', '--json', packageName, 'dist-tags'], (stdout) => {
288+
if (!stdout) {
289+
return {};
290+
}
291+
292+
const parsed = JSON.parse(stdout);
293+
294+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
295+
}),
296+
fetchAndParse(['pm', 'view', '--json', packageName, 'versions'], (stdout) => {
297+
if (!stdout) {
298+
return null;
299+
}
300+
301+
const parsed = JSON.parse(stdout);
302+
303+
return Array.isArray(parsed) ? parsed : [parsed];
304+
}),
305+
]);
306+
307+
if (!versions || versions.length === 0) {
308+
return null;
309+
}
310+
311+
return {
312+
name: packageName,
313+
'dist-tags': (distTags || {}) as Record<string, string>,
314+
versions: versions as string[],
315+
};
316+
},
276317
outputParsers: {
277318
listDependencies: parseBunDependencies,
278319
getRegistryManifest: parseNpmLikeManifest,

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

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -441,19 +441,37 @@ export class PackageManager {
441441
packageName: string,
442442
options: { timeout?: number; registry?: string; bypassCache?: boolean } = {},
443443
): Promise<PackageMetadata | null> {
444-
const commandArgs = [...this.descriptor.getManifestCommand, packageName];
445-
const formatter = this.descriptor.viewCommandFieldArgFormatter;
446-
if (formatter) {
447-
commandArgs.push(...formatter(METADATA_FIELDS));
444+
const cacheKey = options.registry ? `${packageName}|${options.registry}` : packageName;
445+
446+
if (!options.bypassCache) {
447+
const cached = this.#metadataCache.get(cacheKey);
448+
if (cached !== undefined) {
449+
return cached;
450+
}
448451
}
449452

450-
const cacheKey = options.registry ? `${packageName}|${options.registry}` : packageName;
453+
let metadata: PackageMetadata | null;
454+
if (this.descriptor.getRegistryMetadata) {
455+
metadata = await this.descriptor.getRegistryMetadata(packageName, (args, parser) =>
456+
this.#fetchAndParse(args, parser, options),
457+
);
458+
} else {
459+
const commandArgs = [...this.descriptor.getManifestCommand, packageName];
460+
const formatter = this.descriptor.viewCommandFieldArgFormatter;
461+
if (formatter) {
462+
commandArgs.push(...formatter(METADATA_FIELDS));
463+
}
451464

452-
return this.#fetchAndParse(
453-
commandArgs,
454-
(stdout, logger) => this.descriptor.outputParsers.getRegistryMetadata(stdout, logger),
455-
{ ...options, cache: this.#metadataCache, cacheKey },
456-
);
465+
metadata = await this.#fetchAndParse(
466+
commandArgs,
467+
(stdout, logger) => this.descriptor.outputParsers.getRegistryMetadata(stdout, logger),
468+
options,
469+
);
470+
}
471+
472+
this.#metadataCache.set(cacheKey, metadata);
473+
474+
return metadata;
457475
}
458476

459477
/**

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,42 @@ describe('PackageManager', () => {
195195
});
196196
});
197197

198+
describe('getRegistryMetadata', () => {
199+
it('should query dist-tags and versions separately for bun', async () => {
200+
const bunDescriptor = SUPPORTED_PACKAGE_MANAGERS['bun'];
201+
const pm = new PackageManager(host, '/tmp', bunDescriptor);
202+
203+
runCommandSpy.and.callFake((binary, args) => {
204+
if (args.includes('dist-tags')) {
205+
return Promise.resolve({ stdout: JSON.stringify({ latest: '2.0.0' }), stderr: '' });
206+
} else if (args.includes('versions')) {
207+
return Promise.resolve({ stdout: JSON.stringify(['1.0.0', '2.0.0']), stderr: '' });
208+
}
209+
210+
return Promise.resolve({ stdout: '', stderr: '' });
211+
});
212+
213+
const metadata = await pm.getRegistryMetadata('foo');
214+
215+
expect(metadata).toEqual({
216+
name: 'foo',
217+
'dist-tags': { latest: '2.0.0' },
218+
versions: ['1.0.0', '2.0.0'],
219+
});
220+
221+
expect(runCommandSpy).toHaveBeenCalledWith(
222+
'bun',
223+
['pm', 'view', '--json', 'foo', 'dist-tags'],
224+
jasmine.anything(),
225+
);
226+
expect(runCommandSpy).toHaveBeenCalledWith(
227+
'bun',
228+
['pm', 'view', '--json', 'foo', 'versions'],
229+
jasmine.anything(),
230+
);
231+
});
232+
});
233+
198234
describe('initializationError', () => {
199235
it('should throw initializationError when running commands', async () => {
200236
const error = new Error('Not installed');

0 commit comments

Comments
 (0)