diff --git a/config.json b/config.json index dbcde6e..de48e9b 100644 --- a/config.json +++ b/config.json @@ -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" + } } } } diff --git a/package.json b/package.json index ebee274..78b4bac 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/sources/Engine.ts b/sources/Engine.ts index 9ba9998..cc50673 100644 --- a/sources/Engine.ts +++ b/sources/Engine.ts @@ -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() { diff --git a/sources/main.ts b/sources/main.ts index 4d8e18a..ab7fec0 100644 --- a/sources/main.ts +++ b/sources/main.ts @@ -15,16 +15,17 @@ export type Context = BaseContext & CustomContext; export async function main(argv: Array, context: CustomContext & Partial) { 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({binaryName}); - const defaultVersion = await context.engine.getDefaultVersion(firstArg); + const defaultVersion = await context.engine.getDefaultVersion(packageManager); class BinaryCommand extends Command { proxy = Option.Proxy(); @@ -63,7 +64,10 @@ export async function main(argv: Array, 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`); diff --git a/sources/pmmUtils.ts b/sources/pmmUtils.ts index 9a2b1e1..08f4b65 100644 --- a/sources/pmmUtils.ts +++ b/sources/pmmUtils.ts @@ -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> { + 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> { 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: { diff --git a/sources/types.ts b/sources/types.ts index c88bf94..8f19f29 100644 --- a/sources/types.ts +++ b/sources/types.ts @@ -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; } /**