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

View File

@ -1,6 +1,6 @@
{
"name": "corepack",
"version": "0.6.0",
"version": "0.8.0",
"homepage": "https://github.com/nodejs/corepack#readme",
"bugs": {
"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];
if (typeof definition === `undefined`)
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
// from the remote listings
const cachedVersion = await pmmUtils.findInstalledVersion(folderUtils.getInstallFolder(), descriptor);
const cachedVersion = await pmmUtils.findInstalledVersion(folderUtils.getInstallFolder(), finalDescriptor);
if (cachedVersion !== null && useCache)
return {name: descriptor.name, reference: cachedVersion};
return {name: finalDescriptor.name, reference: cachedVersion};
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 => {
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
@ -133,11 +152,11 @@ export class Engine {
resolutionMap.set(entry, range);
const candidates = [...resolutionMap.keys()];
const maxSatisfying = semver.maxSatisfying(candidates, descriptor.range);
const maxSatisfying = semver.maxSatisfying(candidates, finalDescriptor.range);
if (maxSatisfying === null)
return null;
return {name: descriptor.name, reference: maxSatisfying};
return {name: finalDescriptor.name, reference: maxSatisfying};
}
private getLastKnownGoodFile() {

View File

@ -15,16 +15,17 @@ export type Context = BaseContext & CustomContext;
export async function main(argv: Array<string>, context: CustomContext & Partial<Context>) {
const firstArg = argv[0];
const [, candidatePackageManager, requestedVersion] = firstArg.match(/^([^@]*)(?:@(.*))?$/)!;
if (isSupportedPackageManager(firstArg)) {
const packageManager = firstArg;
if (isSupportedPackageManager(candidatePackageManager)) {
const packageManager = candidatePackageManager;
const binaryName = argv[1];
// 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
// based on the arguments.
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> {
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)
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 fs from 'fs';
import path from 'path';
import semver from 'semver';
import {StdioOptions, spawn} from 'child_process';
import fs from 'fs';
import path from 'path';
import semver from 'semver';
import * as debugUtils from './debugUtils';
import * as folderUtils from './folderUtils';
import * as fsUtils from './fsUtils';
import * as httpUtils from './httpUtils';
import {Context} from './main';
import {TagSpec, Descriptor, Locator, PackageManagerSpec} from './types';
import * as debugUtils from './debugUtils';
import * as folderUtils from './folderUtils';
import * as fsUtils from './fsUtils';
import * as httpUtils from './httpUtils';
import {Context} from './main';
import {RegistrySpec, Descriptor, Locator, PackageManagerSpec} from './types';
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) {
case `npm`: {
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`: {
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);
}
default: {

View File

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