diff --git a/sources/Engine.ts b/sources/Engine.ts index d93501596..cc4aca20f 100644 --- a/sources/Engine.ts +++ b/sources/Engine.ts @@ -1,22 +1,23 @@ -import {UsageError} from 'clipanion'; -import fs from 'fs'; -import path from 'path'; -import process from 'process'; -import semverRcompare from 'semver/functions/rcompare'; -import semverValid from 'semver/functions/valid'; -import semverValidRange from 'semver/ranges/valid'; - -import defaultConfig from '../config.json'; - -import * as corepackUtils from './corepackUtils'; -import * as debugUtils from './debugUtils'; -import * as folderUtils from './folderUtils'; -import type {NodeError} from './nodeUtils'; -import * as semverUtils from './semverUtils'; -import * as specUtils from './specUtils'; -import {Config, Descriptor, LazyLocator, Locator} from './types'; -import {SupportedPackageManagers, SupportedPackageManagerSet} from './types'; -import {isSupportedPackageManager, PackageManagerSpec} from './types'; +import { UsageError } from 'clipanion'; +import fs from 'fs'; +import path from 'path'; +import process from 'process'; +import semverRcompare from 'semver/functions/rcompare'; +import semverValid from 'semver/functions/valid'; +import semverParse from 'semver/functions/parse'; +import semverValidRange from 'semver/ranges/valid'; + +import defaultConfig from '../config.json'; + +import * as corepackUtils from './corepackUtils'; +import * as debugUtils from './debugUtils'; +import * as folderUtils from './folderUtils'; +import type { NodeError } from './nodeUtils'; +import * as semverUtils from './semverUtils'; +import * as specUtils from './specUtils'; +import { Config, Descriptor, LazyLocator, Locator } from './types'; +import { SupportedPackageManagers, SupportedPackageManagerSet } from './types'; +import { isSupportedPackageManager, PackageManagerSpec } from './types'; export type PreparedPackageManagerInfo = Awaited>; @@ -71,7 +72,7 @@ export async function getLastKnownGood(): Promise> { async function createLastKnownGoodFile(lastKnownGood: Record) { const content = `${JSON.stringify(lastKnownGood, null, 2)}\n`; - await fs.promises.mkdir(folderUtils.getCorepackHomeFolder(), {recursive: true}); + await fs.promises.mkdir(folderUtils.getCorepackHomeFolder(), { recursive: true }); await fs.promises.writeFile(getLastKnownGoodFilePath(), content, `utf8`); } @@ -162,7 +163,7 @@ export class Engine { const locators: Array = []; for (const name of SupportedPackageManagerSet as Set) - locators.push({name, range: await this.getDefaultVersion(name)}); + locators.push({ name, range: await this.getDefaultVersion(name) }); return locators; } @@ -244,9 +245,9 @@ export class Engine { * project using the default package managers, and configure it so that we * don't need to ask again in the future. */ - async findProjectSpec(initialCwd: string, locator: Locator | LazyLocator, {transparent = false, binaryVersion}: {transparent?: boolean, binaryVersion?: string | null} = {}): Promise { + async findProjectSpec(initialCwd: string, locator: Locator | LazyLocator, { transparent = false, binaryVersion }: { transparent?: boolean, binaryVersion?: string | null } = {}): Promise { // A locator is a valid descriptor (but not the other way around) - const fallbackDescriptor = {name: locator.name, range: `${locator.reference}`}; + const fallbackDescriptor = { name: locator.name, range: `${locator.reference}` }; if (process.env.COREPACK_ENABLE_PROJECT_SPEC === `0`) { if (typeof locator.reference === `function`) @@ -275,7 +276,7 @@ export class Engine { fallbackDescriptor.range = await locator.reference(); if (process.env.COREPACK_ENABLE_AUTO_PIN === `1`) { - const resolved = await this.resolveDescriptor(fallbackDescriptor, {allowTags: true}); + const resolved = await this.resolveDescriptor(fallbackDescriptor, { allowTags: true }); if (resolved === null) throw new UsageError(`Failed to successfully resolve '${fallbackDescriptor.range}' to a valid ${fallbackDescriptor.name} release`); @@ -293,7 +294,7 @@ export class Engine { } case `Found`: { - const spec = result.getSpec({enforceExactVersion: !binaryVersion}); + const spec = result.getSpec({ enforceExactVersion: !binaryVersion }); if (spec.name !== locator.name) { if (transparent) { if (typeof locator.reference === `function`) @@ -313,7 +314,7 @@ export class Engine { } } - async executePackageManagerRequest({packageManager, binaryName, binaryVersion}: PackageManagerRequest, {cwd, args}: {cwd: string, args: Array}): Promise { + async executePackageManagerRequest({ packageManager, binaryName, binaryVersion }: PackageManagerRequest, { cwd, args }: { cwd: string, args: Array }): Promise { let fallbackLocator: Locator | LazyLocator = { name: binaryName as SupportedPackageManagers, reference: undefined as any, @@ -344,12 +345,12 @@ export class Engine { }; } - const descriptor = await this.findProjectSpec(cwd, fallbackLocator, {transparent: isTransparentCommand, binaryVersion}); + const descriptor = await this.findProjectSpec(cwd, fallbackLocator, { transparent: isTransparentCommand, binaryVersion }); if (binaryVersion) descriptor.range = binaryVersion; - const resolved = await this.resolveDescriptor(descriptor, {allowTags: true}); + const resolved = await this.resolveDescriptor(descriptor, { allowTags: true }); if (resolved === null) throw new UsageError(`Failed to successfully resolve '${descriptor.range}' to a valid ${descriptor.name} release`); @@ -358,7 +359,7 @@ export class Engine { return await corepackUtils.runVersion(resolved, installSpec, binaryName, args); } - async resolveDescriptor(descriptor: Descriptor, {allowTags = false, useCache = true}: {allowTags?: boolean, useCache?: boolean} = {}): Promise { + async resolveDescriptor(descriptor: Descriptor, { allowTags = false, useCache = true }: { allowTags?: boolean, useCache?: boolean } = {}): Promise { if (!corepackUtils.isSupportedPackageManagerDescriptor(descriptor)) { if (process.env.COREPACK_ENABLE_UNSAFE_CUSTOM_URLS !== `1` && isSupportedPackageManager(descriptor.name)) throw new UsageError(`Illegal use of URL for known package manager. Instead, select a specific version, or set COREPACK_ENABLE_UNSAFE_CUSTOM_URLS=1 in your environment (${descriptor.name}@${descriptor.range})`); @@ -399,25 +400,37 @@ export class Engine { // from the remote listings const cachedVersion = await corepackUtils.findInstalledVersion(folderUtils.getInstallFolder(), finalDescriptor); if (cachedVersion !== null && useCache) - return {name: finalDescriptor.name, reference: cachedVersion}; + return { name: finalDescriptor.name, reference: cachedVersion }; // If the user asked for a specific version, no need to request the list of // available versions from the registry. if (semverValid(finalDescriptor.range)) - return {name: finalDescriptor.name, reference: finalDescriptor.range}; + return { name: finalDescriptor.name, reference: finalDescriptor.range }; const versions = await Promise.all(Object.keys(definition.ranges).map(async range => { const packageManagerSpec = definition.ranges[range]; const registry = corepackUtils.getRegistryFromPackageManagerSpec(packageManagerSpec); - const versions = await corepackUtils.fetchAvailableVersions(registry); - return versions.filter(version => semverUtils.satisfiesWithPrereleases(version, finalDescriptor.range)); + return await corepackUtils.fetchAvailableVersions(registry); })); - const highestVersion = [...new Set(versions.flat())].sort(semverRcompare); - if (highestVersion.length === 0) + const allVersions = [...new Set(versions.flat())]; + const matchingVersions = allVersions.filter(version => semverUtils.satisfiesWithPrereleases(version, finalDescriptor.range)); + + if (matchingVersions.length === 0) return null; - return {name: finalDescriptor.name, reference: highestVersion[0]}; + const stableMatchingVersions = matchingVersions.filter(version => { + const semver = semverParse(version); + return semver && semver.prerelease.length === 0; + }); + + const candidates = stableMatchingVersions.length > 0 && !semverUtils.isPrereleaseRange(finalDescriptor.range) && !definition.ranges[finalDescriptor.range] + ? stableMatchingVersions + : matchingVersions; + + candidates.sort(semverRcompare); + + return { name: finalDescriptor.name, reference: candidates[0] }; } } diff --git a/sources/semverUtils.ts b/sources/semverUtils.ts index 9e138ed3c..b5bc93fdb 100644 --- a/sources/semverUtils.ts +++ b/sources/semverUtils.ts @@ -1,4 +1,4 @@ -import Range from 'semver/classes/range'; +import Range from 'semver/classes/range'; import SemVer from 'semver/classes/semver'; /** @@ -47,3 +47,22 @@ export function satisfiesWithPrereleases(version: string | null, range: string, }); }); } + +/** + * Returns whether the given range contains any prerelease versions. + * + * This is used to decide whether we should prefer stable versions or + * consider prereleases when resolving a range. + */ +export function isPrereleaseRange(range: string): boolean { + try { + const semverRange = new Range(range); + return semverRange.set.some((comparatorSet: any) => { + return comparatorSet.some((comparator: any) => { + return comparator.semver.prerelease.length > 0; + }); + }); + } catch (err) { + return false; + } +}