Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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: <T>(
args: readonly string[],
parser: (stdout: string, logger?: Logger) => T | null,
) => Promise<T | null>,
) => Promise<PackageMetadata | null>;

/** A collection of functions to parse the output of specific commands. */
readonly outputParsers: {
/** A function to parse the output of `listDependenciesCommand`. */
Expand Down Expand Up @@ -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<string, string>,
versions: versions as string[],
};
Comment thread
clydin marked this conversation as resolved.
},
outputParsers: {
listDependencies: parseBunDependencies,
getRegistryManifest: parseNpmLikeManifest,
Expand Down
38 changes: 28 additions & 10 deletions packages/angular/cli/src/package-managers/package-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -441,19 +441,37 @@ export class PackageManager {
packageName: string,
options: { timeout?: number; registry?: string; bypassCache?: boolean } = {},
): Promise<PackageMetadata | null> {
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;
}

/**
Expand Down
36 changes: 36 additions & 0 deletions packages/angular/cli/src/package-managers/package-manager_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
Loading