Skip to content

Commit c7a7f19

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 bdc3d2f commit c7a7f19

3 files changed

Lines changed: 101 additions & 10 deletions

File tree

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

Lines changed: 37 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,34 @@ 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 null;
290+
}
291+
292+
return JSON.parse(stdout);
293+
}),
294+
fetchAndParse(['pm', 'view', '--json', packageName, 'versions'], (stdout) => {
295+
if (!stdout) {
296+
return null;
297+
}
298+
299+
return JSON.parse(stdout);
300+
}),
301+
]);
302+
303+
if (!distTags || !versions) {
304+
return null;
305+
}
306+
307+
return {
308+
name: packageName,
309+
'dist-tags': distTags as Record<string, string>,
310+
versions: versions as string[],
311+
};
312+
},
276313
outputParsers: {
277314
listDependencies: parseBunDependencies,
278315
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 = 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
@@ -86,6 +86,42 @@ describe('PackageManager', () => {
8686
});
8787
});
8888

89+
describe('getRegistryMetadata', () => {
90+
it('should query dist-tags and versions separately for bun', async () => {
91+
const bunDescriptor = SUPPORTED_PACKAGE_MANAGERS['bun'];
92+
const pm = new PackageManager(host, '/tmp', bunDescriptor);
93+
94+
runCommandSpy.and.callFake((binary, args) => {
95+
if (args.includes('dist-tags')) {
96+
return Promise.resolve({ stdout: JSON.stringify({ latest: '2.0.0' }), stderr: '' });
97+
} else if (args.includes('versions')) {
98+
return Promise.resolve({ stdout: JSON.stringify(['1.0.0', '2.0.0']), stderr: '' });
99+
}
100+
101+
return Promise.resolve({ stdout: '', stderr: '' });
102+
});
103+
104+
const metadata = await pm.getRegistryMetadata('foo');
105+
106+
expect(metadata).toEqual({
107+
name: 'foo',
108+
'dist-tags': { latest: '2.0.0' },
109+
versions: ['1.0.0', '2.0.0'],
110+
});
111+
112+
expect(runCommandSpy).toHaveBeenCalledWith(
113+
'bun',
114+
['pm', 'view', '--json', 'foo', 'dist-tags'],
115+
jasmine.anything(),
116+
);
117+
expect(runCommandSpy).toHaveBeenCalledWith(
118+
'bun',
119+
['pm', 'view', '--json', 'foo', 'versions'],
120+
jasmine.anything(),
121+
);
122+
});
123+
});
124+
89125
describe('initializationError', () => {
90126
it('should throw initializationError when running commands', async () => {
91127
const error = new Error('Not installed');

0 commit comments

Comments
 (0)