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
85 changes: 49 additions & 36 deletions sources/Engine.ts
Original file line number Diff line number Diff line change
@@ -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<ReturnType<Engine[`ensurePackageManager`]>>;

Expand Down Expand Up @@ -71,7 +72,7 @@ export async function getLastKnownGood(): Promise<Record<string, string>> {

async function createLastKnownGoodFile(lastKnownGood: Record<string, string>) {
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`);
}

Expand Down Expand Up @@ -162,7 +163,7 @@ export class Engine {
const locators: Array<Descriptor> = [];

for (const name of SupportedPackageManagerSet as Set<SupportedPackageManagers>)
locators.push({name, range: await this.getDefaultVersion(name)});
locators.push({ name, range: await this.getDefaultVersion(name) });

return locators;
}
Expand Down Expand Up @@ -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<Descriptor> {
async findProjectSpec(initialCwd: string, locator: Locator | LazyLocator, { transparent = false, binaryVersion }: { transparent?: boolean, binaryVersion?: string | null } = {}): Promise<Descriptor> {
// 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`)
Expand Down Expand Up @@ -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`);

Expand All @@ -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`)
Expand All @@ -313,7 +314,7 @@ export class Engine {
}
}

async executePackageManagerRequest({packageManager, binaryName, binaryVersion}: PackageManagerRequest, {cwd, args}: {cwd: string, args: Array<string>}): Promise<void> {
async executePackageManagerRequest({ packageManager, binaryName, binaryVersion }: PackageManagerRequest, { cwd, args }: { cwd: string, args: Array<string> }): Promise<void> {
let fallbackLocator: Locator | LazyLocator = {
name: binaryName as SupportedPackageManagers,
reference: undefined as any,
Expand Down Expand Up @@ -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`);

Expand All @@ -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<Locator | null> {
async resolveDescriptor(descriptor: Descriptor, { allowTags = false, useCache = true }: { allowTags?: boolean, useCache?: boolean } = {}): Promise<Locator | null> {
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})`);
Expand Down Expand Up @@ -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] };
}
}
21 changes: 20 additions & 1 deletion sources/semverUtils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Range from 'semver/classes/range';
import Range from 'semver/classes/range';
import SemVer from 'semver/classes/semver';

/**
Expand Down Expand Up @@ -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;
}
}