Adds support for executing arbitrary versions of packages managers (#39)

This commit is contained in:
Maël Nison 2021-07-05 22:11:23 +02:00 committed by GitHub
parent b77ed59321
commit a11e796f7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 82 additions and 37 deletions

View File

@ -15,7 +15,7 @@
"npm": "./bin/npm-cli.js", "npm": "./bin/npm-cli.js",
"npx": "./bin/npx-cli.js" "npx": "./bin/npx-cli.js"
}, },
"tags": { "registry": {
"type": "npm", "type": "npm",
"package": "npm" "package": "npm"
} }
@ -37,7 +37,7 @@
"pnpm": "./bin/pnpm.js", "pnpm": "./bin/pnpm.js",
"pnpx": "./bin/pnpx.js" "pnpx": "./bin/pnpx.js"
}, },
"tags": { "registry": {
"type": "npm", "type": "npm",
"package": "pnpm" "package": "pnpm"
} }
@ -48,7 +48,7 @@
"pnpm": "./bin/pnpm.cjs", "pnpm": "./bin/pnpm.cjs",
"pnpx": "./bin/pnpx.cjs" "pnpx": "./bin/pnpx.cjs"
}, },
"tags": { "registry": {
"type": "npm", "type": "npm",
"package": "pnpm" "package": "pnpm"
} }
@ -70,7 +70,7 @@
"yarn": "./bin/yarn.js", "yarn": "./bin/yarn.js",
"yarnpkg": "./bin/yarn.js" "yarnpkg": "./bin/yarn.js"
}, },
"tags": { "registry": {
"type": "npm", "type": "npm",
"package": "yarn" "package": "yarn"
} }
@ -79,10 +79,13 @@
"name": "yarn", "name": "yarn",
"url": "https://repo.yarnpkg.com/{}/packages/yarnpkg-cli/bin/yarn.js", "url": "https://repo.yarnpkg.com/{}/packages/yarnpkg-cli/bin/yarn.js",
"bin": ["yarn", "yarnpkg"], "bin": ["yarn", "yarnpkg"],
"tags": { "registry": {
"type": "url", "type": "url",
"url": "https://repo.yarnpkg.com/tags", "url": "https://repo.yarnpkg.com/tags",
"field": "tags" "fields": {
"tags": "latest",
"versions": "tags"
}
} }
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "corepack", "name": "corepack",
"version": "0.6.0", "version": "0.8.0",
"homepage": "https://github.com/nodejs/corepack#readme", "homepage": "https://github.com/nodejs/corepack#readme",
"bugs": { "bugs": {
"url": "https://github.com/nodejs/corepack/issues" "url": "https://github.com/nodejs/corepack/issues"

View File

@ -105,23 +105,42 @@ export class Engine {
}; };
} }
async resolveDescriptor(descriptor: Descriptor, {useCache = true}: {useCache?: boolean} = {}) { async resolveDescriptor(descriptor: Descriptor, {allowTags = false, useCache = true}: {allowTags?: boolean, useCache?: boolean} = {}) {
const definition = this.config.definitions[descriptor.name]; const definition = this.config.definitions[descriptor.name];
if (typeof definition === `undefined`) if (typeof definition === `undefined`)
throw new UsageError(`This package manager (${descriptor.name}) isn't supported by this corepack build`); throw new UsageError(`This package manager (${descriptor.name}) isn't supported by this corepack build`);
let finalDescriptor = descriptor;
if (descriptor.range.match(/^[a-z-]+$/)) {
if (!allowTags)
throw new UsageError(`Packages managers can't be referended via tags in this context`);
// We only resolve tags from the latest registry entry
const ranges = Object.keys(definition.ranges);
const tagRange = ranges[ranges.length - 1];
const tags = await pmmUtils.fetchAvailableTags(definition.ranges[tagRange].registry);
if (!Object.prototype.hasOwnProperty.call(tags, descriptor.range))
throw new UsageError(`Tag not found (${descriptor.range})`);
finalDescriptor = {
name: descriptor.name,
range: tags[descriptor.range],
};
}
// If a compatible version is already installed, no need to query one // If a compatible version is already installed, no need to query one
// from the remote listings // from the remote listings
const cachedVersion = await pmmUtils.findInstalledVersion(folderUtils.getInstallFolder(), descriptor); const cachedVersion = await pmmUtils.findInstalledVersion(folderUtils.getInstallFolder(), finalDescriptor);
if (cachedVersion !== null && useCache) if (cachedVersion !== null && useCache)
return {name: descriptor.name, reference: cachedVersion}; return {name: finalDescriptor.name, reference: cachedVersion};
const candidateRangeDefinitions = Object.keys(definition.ranges).filter(range => { const candidateRangeDefinitions = Object.keys(definition.ranges).filter(range => {
return semverUtils.satisfiesWithPrereleases(descriptor.range, range); return semverUtils.satisfiesWithPrereleases(finalDescriptor.range, range);
}); });
const tagResolutions = await Promise.all(candidateRangeDefinitions.map(async range => { const tagResolutions = await Promise.all(candidateRangeDefinitions.map(async range => {
return [range, await pmmUtils.fetchAvailableVersions(definition.ranges[range].tags)] as const; return [range, await pmmUtils.fetchAvailableVersions(definition.ranges[range].registry)] as const;
})); }));
// If a version is available under multiple strategies (for example if // If a version is available under multiple strategies (for example if
@ -133,11 +152,11 @@ export class Engine {
resolutionMap.set(entry, range); resolutionMap.set(entry, range);
const candidates = [...resolutionMap.keys()]; const candidates = [...resolutionMap.keys()];
const maxSatisfying = semver.maxSatisfying(candidates, descriptor.range); const maxSatisfying = semver.maxSatisfying(candidates, finalDescriptor.range);
if (maxSatisfying === null) if (maxSatisfying === null)
return null; return null;
return {name: descriptor.name, reference: maxSatisfying}; return {name: finalDescriptor.name, reference: maxSatisfying};
} }
private getLastKnownGoodFile() { private getLastKnownGoodFile() {

View File

@ -15,16 +15,17 @@ export type Context = BaseContext & CustomContext;
export async function main(argv: Array<string>, context: CustomContext & Partial<Context>) { export async function main(argv: Array<string>, context: CustomContext & Partial<Context>) {
const firstArg = argv[0]; const firstArg = argv[0];
const [, candidatePackageManager, requestedVersion] = firstArg.match(/^([^@]*)(?:@(.*))?$/)!;
if (isSupportedPackageManager(firstArg)) { if (isSupportedPackageManager(candidatePackageManager)) {
const packageManager = firstArg; const packageManager = candidatePackageManager;
const binaryName = argv[1]; const binaryName = argv[1];
// Note: we're playing a bit with Clipanion here, since instead of letting it // Note: we're playing a bit with Clipanion here, since instead of letting it
// decide how to route the commands, we'll instead tweak the init settings // decide how to route the commands, we'll instead tweak the init settings
// based on the arguments. // based on the arguments.
const cli = new Cli<Context>({binaryName}); const cli = new Cli<Context>({binaryName});
const defaultVersion = await context.engine.getDefaultVersion(firstArg); const defaultVersion = await context.engine.getDefaultVersion(packageManager);
class BinaryCommand extends Command<Context> { class BinaryCommand extends Command<Context> {
proxy = Option.Proxy(); proxy = Option.Proxy();
@ -63,7 +64,10 @@ export async function main(argv: Array<string>, context: CustomContext & Partial
} }
} }
const resolved = await context.engine.resolveDescriptor(descriptor); if (requestedVersion)
descriptor.range = requestedVersion;
const resolved = await context.engine.resolveDescriptor(descriptor, {allowTags: true});
if (resolved === null) if (resolved === null)
throw new UsageError(`Failed to successfully resolve '${descriptor.range}' to a valid ${descriptor.name} release`); throw new UsageError(`Failed to successfully resolve '${descriptor.range}' to a valid ${descriptor.name} release`);

View File

@ -1,18 +1,34 @@
import {StdioOptions, spawn} from 'child_process'; import {StdioOptions, spawn} from 'child_process';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import semver from 'semver'; import semver from 'semver';
import * as debugUtils from './debugUtils'; import * as debugUtils from './debugUtils';
import * as folderUtils from './folderUtils'; import * as folderUtils from './folderUtils';
import * as fsUtils from './fsUtils'; import * as fsUtils from './fsUtils';
import * as httpUtils from './httpUtils'; import * as httpUtils from './httpUtils';
import {Context} from './main'; import {Context} from './main';
import {TagSpec, Descriptor, Locator, PackageManagerSpec} from './types'; import {RegistrySpec, Descriptor, Locator, PackageManagerSpec} from './types';
declare const __non_webpack_require__: unknown; declare const __non_webpack_require__: unknown;
export async function fetchAvailableVersions(spec: TagSpec) { export async function fetchAvailableTags(spec: RegistrySpec): Promise<Record<string, string>> {
switch (spec.type) {
case `npm`: {
const data = await httpUtils.fetchAsJson(`https://registry.npmjs.org/${spec.package}`, {headers: {[`Accept`]: `application/vnd.npm.install-v1+json`}});
return data[`dist-tags`];
}
case `url`: {
const data = await httpUtils.fetchAsJson(spec.url);
return data[spec.fields.tags];
}
default: {
throw new Error(`Unsupported specification ${JSON.stringify(spec)}`);
}
}
}
export async function fetchAvailableVersions(spec: RegistrySpec): Promise<Array<string>> {
switch (spec.type) { switch (spec.type) {
case `npm`: { case `npm`: {
const data = await httpUtils.fetchAsJson(`https://registry.npmjs.org/${spec.package}`, {headers: {[`Accept`]: `application/vnd.npm.install-v1+json`}}); const data = await httpUtils.fetchAsJson(`https://registry.npmjs.org/${spec.package}`, {headers: {[`Accept`]: `application/vnd.npm.install-v1+json`}});
@ -20,7 +36,7 @@ export async function fetchAvailableVersions(spec: TagSpec) {
} }
case `url`: { case `url`: {
const data = await httpUtils.fetchAsJson(spec.url); const data = await httpUtils.fetchAsJson(spec.url);
const field = data[spec.field]; const field = data[spec.fields.versions];
return Array.isArray(field) ? field : Object.keys(field); return Array.isArray(field) ? field : Object.keys(field);
} }
default: { default: {

View File

@ -22,20 +22,23 @@ export function isSupportedPackageManager(value: string): value is SupportedPack
return SupportedPackageManagerSet.has(value as SupportedPackageManagers); return SupportedPackageManagerSet.has(value as SupportedPackageManagers);
} }
export interface NpmTagSpec { export interface NpmRegistrySpec {
type: `npm`; type: `npm`;
package: string; package: string;
} }
export interface UrlTagSpec { export interface UrlRegistrySpec {
type: `url`; type: `url`;
url: string; url: string;
field: string; fields: {
tags: string;
versions: string;
};
} }
export type TagSpec = export type RegistrySpec =
| NpmTagSpec | NpmRegistrySpec
| UrlTagSpec; | UrlRegistrySpec;
/** /**
* Defines how the package manager is meant to be downloaded and accessed. * Defines how the package manager is meant to be downloaded and accessed.
@ -43,7 +46,7 @@ export type TagSpec =
export interface PackageManagerSpec { export interface PackageManagerSpec {
url: string; url: string;
bin: BinSpec | BinList; bin: BinSpec | BinList;
tags: TagSpec; registry: RegistrySpec;
} }
/** /**