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
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,9 @@ Depending on the value of `devEngines.packageManager.onFail`:
of mismatch.

If the top-level `packageManager` field is missing, Corepack will use the
package manager defined in `devEngines.packageManager` – in which case you must
provide a specific version in `devEngines.packageManager.version`, ideally with
a hash, as explained in the previous section:
package manager defined in `devEngines.packageManager`. You should provide a
specific version in `devEngines.packageManager.version`, ideally with a hash, as
explained in the previous section:

```json
{
Expand All @@ -142,6 +142,16 @@ a hash, as explained in the previous section:
}
```

When `devEngines.packageManager.version` is a range rather than a specific
version, Corepack resolves it the same way as when a range is given on the
command line: the latest version matching the range is looked up on the npm
registry, which means the resolution requires network access (or a cache
containing a matching version, see [Offline Workflow](#offline-workflow)), and
may change over time. Set `COREPACK_ENABLE_AUTO_PIN=1` to have Corepack add the
resolved version to the `packageManager` field. When
`devEngines.packageManager.version` is missing, Corepack falls back to its
[Known Good Release](#known-good-releases) for that package manager.

## Known Good Releases

When running Corepack within projects that don't list a supported package
Expand Down
12 changes: 10 additions & 2 deletions sources/Engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,15 @@ export class Engine {
}

case `NoSpec`: {
if (typeof locator.reference === `function`)
const {devEnginesValue} = result;
const nameMatches = devEnginesValue != null && devEnginesValue.name === fallbackDescriptor.name;

if (devEnginesValue != null && !nameMatches && !transparent)
specUtils.warnOrThrow(`This project is configured to use ${devEnginesValue.name} because ${result.target} has a "devEngines.packageManager" field`, devEnginesValue.onFail);

if (nameMatches && devEnginesValue.version)
fallbackDescriptor.range = devEnginesValue.version;
else if (typeof locator.reference === `function`)
fallbackDescriptor.range = await locator.reference();

if (process.env.COREPACK_ENABLE_AUTO_PIN === `1`) {
Expand Down Expand Up @@ -307,7 +315,7 @@ export class Engine {
debugUtils.log(`Falling back to ${fallbackDescriptor.name}@${fallbackDescriptor.range} in a ${spec.name}@${spec.range} project`);
return fallbackDescriptor;
} else {
throw new UsageError(`This project is configured to use ${spec.name} because ${result.target} has a "packageManager" field`);
throw new UsageError(`This project is configured to use ${spec.name} because ${result.target} has a "${result.field}" field`);
}
} else {
debugUtils.log(`Using ${spec.name}@${spec.range} as defined in project manifest ${result.target}`);
Expand Down
12 changes: 9 additions & 3 deletions sources/commands/Base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,17 @@ export abstract class BaseCommand extends Command<Context> {
case `NoProject`:
throw new UsageError(`Couldn't find a project in the local directory - please specify the package manager to pack, or run this command from a valid project`);

case `NoSpec`:
throw new UsageError(`The local project doesn't feature a 'packageManager' field nor a 'devEngines.packageManager' field - please specify the package manager to pack, or update the manifest to reference it`);
case `NoSpec`: {
const {devEnginesValue} = lookup;
if (devEnginesValue?.version)
return [specUtils.devEnginesToDescriptor(devEnginesValue)];

throw new UsageError(`The local project doesn't feature a 'packageManager' field ${devEnginesValue ? `` : `nor a 'devEngines.packageManager' field `}- please specify the package manager to pack, or update the manifest to reference it`);
}

default: {
return [lookup.range ?? lookup.getSpec()];
const {devEnginesValue} = lookup;
return [devEnginesValue?.version ? specUtils.devEnginesToDescriptor(devEnginesValue) : lookup.getSpec()];
}
}
} else {
Expand Down
10 changes: 8 additions & 2 deletions sources/commands/deprecated/Prepare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,14 @@ export class PrepareCommand extends Command<Context> {
case `NoProject`:
throw new UsageError(`Couldn't find a project in the local directory - please specify the package manager to pack, or run this command from a valid project`);

case `NoSpec`:
throw new UsageError(`The local project doesn't feature a 'packageManager' field - please specify the package manager to pack, or update the manifest to reference it`);
case `NoSpec`: {
const {devEnginesValue} = lookup;
if (devEnginesValue?.version) {
specs.push(specUtils.devEnginesToDescriptor(devEnginesValue));
break;
}
throw new UsageError(`The local project doesn't feature a 'packageManager' field ${devEnginesValue ? `` : `nor a 'devEngines.packageManager' field `}- please specify the package manager to pack, or update the manifest to reference it`);
}

default: {
specs.push(lookup.getSpec());
Expand Down
100 changes: 66 additions & 34 deletions sources/specUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,23 @@ type CorepackPackageJSON = {
devEngines?: {packageManager?: DevEngineDependency};
};

interface DevEngineDependency {
export interface DevEngineDependency {
name: string;
version: string;
/** Semver version or range, as found in the manifest. */
version?: string;
onFail?: `ignore` | `warn` | `error`;
}
function warnOrThrow(errorMessage: string, onFail?: DevEngineDependency[`onFail`]) {

export function devEnginesToDescriptor({name, version}: DevEngineDependency): Descriptor {
return {name, range: version ?? `*`};
}

interface ParsedPackageJSON {
packageManagerField?: string;
devEnginesPackageManager?: DevEngineDependency;
}

export function warnOrThrow(errorMessage: string, onFail?: DevEngineDependency[`onFail`]) {
switch (onFail) {
case `ignore`:
break;
Expand All @@ -77,66 +88,64 @@ function warnOrThrow(errorMessage: string, onFail?: DevEngineDependency[`onFail`
console.warn(`! Corepack validation warning: ${errorMessage}`);
}
}
function parsePackageJSON(packageJSONContent: CorepackPackageJSON) {
function parsePackageJSON(packageJSONContent: CorepackPackageJSON): ParsedPackageJSON {
const {packageManager: pm} = packageJSONContent;
if (packageJSONContent.devEngines?.packageManager != null) {
const {packageManager} = packageJSONContent.devEngines;

if (typeof packageManager !== `object`) {
console.warn(`! Corepack only supports objects as valid value for devEngines.packageManager. The current value (${JSON.stringify(packageManager)}) will be ignored.`);
return pm;
return {packageManagerField: pm};
}
if (Array.isArray(packageManager)) {
console.warn(`! Corepack does not currently support array values for devEngines.packageManager`);
return pm;
return {packageManagerField: pm};
}

const {name, version, onFail} = packageManager;
if (typeof name !== `string` || name.includes(`@`)) {
warnOrThrow(`The value of devEngines.packageManager.name ${JSON.stringify(name)} is not a supported string value`, onFail);
return pm;
return {packageManagerField: pm};
}
if (version != null && (typeof version !== `string` || !semverValidRange(version))) {
warnOrThrow(`The value of devEngines.packageManager.version ${JSON.stringify(version)} is not a valid semver range`, onFail);
return pm;
return {packageManagerField: pm};
}

debugUtils.log(`devEngines.packageManager defines that ${name}@${version} is the local package manager`);
debugUtils.log(`devEngines.packageManager defines that ${name}${version ? `@${version}` : ``} should be the local package manager`);

if (pm) {
if (!pm.startsWith?.(`${name}@`))
if (!pm.startsWith?.(`${name}@`)) {
warnOrThrow(`"packageManager" field is set to ${JSON.stringify(pm)} which does not match the "devEngines.packageManager" field set to ${JSON.stringify(name)}`, onFail);

else if (version != null && !semverSatisfies(pm.slice(packageManager.name.length + 1), version))
} else if (version != null && !semverSatisfies(pm.slice(name.length + 1), version)) {
warnOrThrow(`"packageManager" field is set to ${JSON.stringify(pm)} which does not match the value defined in "devEngines.packageManager" for ${JSON.stringify(name)} of ${JSON.stringify(version)}`, onFail);

return pm;
}
}


return `${name}@${version ?? `*`}`;
return {packageManagerField: pm, devEnginesPackageManager: {name, version, onFail}};
}

return pm;
return {packageManagerField: pm};
}

export async function setLocalPackageManager(cwd: string, info: PreparedPackageManagerInfo) {
const lookup = await loadSpecAndEnv(cwd);

const range = `range` in lookup && lookup.range;
if (range) {
if (info.locator.name !== range.name || !semverSatisfies(info.locator.reference, range.range)) {
warnOrThrow(`The requested version of ${info.locator.name}@${info.locator.reference} does not match the devEngines specification (${range.name}@${range.range})`, range.onFail);
const projectFound = lookup.type !== `NoProject`;
const devEnginesValue = projectFound ? lookup.devEnginesValue : undefined;
if (devEnginesValue) {
if (info.locator.name !== devEnginesValue.name || (devEnginesValue.version != null && !semverSatisfies(info.locator.reference, devEnginesValue.version))) {
warnOrThrow(`The requested version of ${info.locator.name}@${info.locator.reference} does not match the devEngines specification (${devEnginesValue.name}@${devEnginesValue.version ?? `*`})`, devEnginesValue.onFail);
}
}

const content = lookup.type !== `NoProject`
const content = projectFound
? await fs.promises.readFile(lookup.target, `utf8`)
: ``;

const {data, indent} = nodeUtils.readPackageJson(content);

const previousPackageManager = data.packageManager ?? (range ? `${range.name}@${range.range}` : `unknown`);
const previousPackageManager = data.packageManager ?? (devEnginesValue ? `${devEnginesValue.name}@${devEnginesValue.version ?? `*`}` : `unknown`);
data.packageManager = `${info.locator.name}@${info.locator.reference}`;

const newContent = nodeUtils.normalizeLineEndings(content, `${JSON.stringify(data, null, indent)}\n`);
Expand All @@ -150,13 +159,15 @@ export async function setLocalPackageManager(cwd: string, info: PreparedPackageM
interface FoundSpecResult {
type: `Found`;
target: string;
/** Name of the `package.json` field the spec was read from. */
field: `packageManager` | `devEngines.packageManager`;
getSpec: (options?: {enforceExactVersion?: boolean}) => Descriptor;
range?: Descriptor & {onFail?: DevEngineDependency[`onFail`]};
devEnginesValue?: DevEngineDependency;
envFilePath?: string;
}
export type LoadSpecResult =
| {type: `NoProject`, target: string, envFilePath?: string}
| {type: `NoSpec`, target: string, envFilePath?: string}
| {type: `NoSpec`, target: string, envFilePath?: string, devEnginesValue?: DevEngineDependency}
| FoundSpecResult;

async function loadEnvFileIfExists(cwd: string): Promise<{env: LocalEnvFile, path: string} | void> {
Expand Down Expand Up @@ -234,22 +245,43 @@ export async function loadSpecAndEnv(initialCwd: string, {envOnly} = {envOnly: f
if (selection === null)
return {type: `NoProject`, target: path.join(initialCwd, `package.json`), envFilePath: localEnv?.path};

const rawPmSpec = parsePackageJSON(selection.data);
if (typeof rawPmSpec === `undefined`)
const {packageManagerField, devEnginesPackageManager} = parsePackageJSON(selection.data);

if (devEnginesPackageManager != null && !packageManagerField) {
const {name, version} = devEnginesPackageManager;

// Without an exact version, there is nothing to install yet – the range (if
// any) is resolved by the caller, as it would for a project without spec.
if (!version || !semverValid(version)) {
debugUtils.log(`${selection.manifestPath} defines ${name} as local package manager using devEngines.packageManager, without an exact version`);
return {type: `NoSpec`, target: selection.manifestPath, envFilePath: localEnv?.path, devEnginesValue: devEnginesPackageManager};
}

debugUtils.log(`${selection.manifestPath} defines ${name}@${version} as local package manager using devEngines.packageManager`);

return {
type: `Found`,
target: selection.manifestPath,
field: `devEngines.packageManager`,
envFilePath: localEnv?.path,
devEnginesValue: devEnginesPackageManager,
// Lazy-loading it so we do not throw errors on commands that do not need valid spec.
getSpec: ({enforceExactVersion = true} = {}) => parseSpec(`${name}@${version}`, path.relative(initialCwd, selection.manifestPath), {enforceExactVersion}),
};
}

if (packageManagerField === undefined)
return {type: `NoSpec`, target: selection.manifestPath, envFilePath: localEnv?.path};

debugUtils.log(`${selection.manifestPath} defines ${rawPmSpec} as local package manager`);
debugUtils.log(`${selection.manifestPath} defines ${packageManagerField} as local package manager using the packageManager field`);

return {
type: `Found`,
target: selection.manifestPath,
field: `packageManager`,
envFilePath: localEnv?.path,
range: selection.data.devEngines?.packageManager?.version && {
name: selection.data.devEngines.packageManager.name,
range: selection.data.devEngines.packageManager.version,
onFail: selection.data.devEngines.packageManager.onFail,
},
devEnginesValue: devEnginesPackageManager,
// Lazy-loading it so we do not throw errors on commands that do not need valid spec.
getSpec: ({enforceExactVersion = true} = {}) => parseSpec(rawPmSpec, path.relative(initialCwd, selection.manifestPath), {enforceExactVersion}),
getSpec: ({enforceExactVersion = true} = {}) => parseSpec(packageManagerField, path.relative(initialCwd, selection.manifestPath), {enforceExactVersion}),
};
}
Loading