From 6432b7486eeac6264346c9a096c936579c64abfa Mon Sep 17 00:00:00 2001 From: michkot <14879243+michkot@users.noreply.github.com> Date: Sat, 18 Apr 2026 00:06:36 +0200 Subject: [PATCH 01/12] test: reproduce devEngines manifest selection bug in CLI --- tests/main.test.ts | 82 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/tests/main.test.ts b/tests/main.test.ts index c629f002f..49d395d4c 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -582,6 +582,88 @@ it(`should use the closest matching packageManager field`, async () => { }); }); +it(`should use the closest matching devEngines.packageManager field when parent has no spec`, async () => { + await xfs.mktempPromise(async cwd => { + const projectCwd = ppath.join(cwd, `foo` as PortablePath); + + await xfs.mkdirPromise(projectCwd, {recursive: true}); + + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as PortablePath), { + // empty package.json file + }); + + await xfs.writeJsonPromise(ppath.join(projectCwd, `package.json` as PortablePath), { + devEngines: { + packageManager: { + name: `npm`, + version: `6.14.2`, + }, + }, + }); + + await expect(runCli(projectCwd, [`npm`, `--version`])).resolves.toMatchObject({ + exitCode: 0, + stderr: ``, + stdout: `6.14.2\n`, + }); + }); +}); + +it(`should use the closest matching devEngines.packageManager field over a parent packageManager field`, async () => { + await xfs.mktempPromise(async cwd => { + const projectCwd = ppath.join(cwd, `foo` as PortablePath); + + await xfs.mkdirPromise(projectCwd, {recursive: true}); + + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as PortablePath), { + packageManager: `yarn@1.22.4`, + }); + + await xfs.writeJsonPromise(ppath.join(projectCwd, `package.json` as PortablePath), { + devEngines: { + packageManager: { + name: `npm`, + version: `6.14.2`, + }, + }, + }); + + await expect(runCli(projectCwd, [`npm`, `--version`])).resolves.toMatchObject({ + exitCode: 0, + stderr: ``, + stdout: `6.14.2\n`, + }); + }); +}); + +it(`should ignore a parent packageManager when a closer devEngines.packageManager is invalid but non-fatal`, async () => { + await xfs.mktempPromise(async cwd => { + const projectCwd = ppath.join(cwd, `foo` as PortablePath); + + await xfs.mkdirPromise(projectCwd, {recursive: true}); + + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as PortablePath), { + packageManager: `yarn@1.22.4`, + }); + + await xfs.writeJsonPromise(ppath.join(projectCwd, `package.json` as PortablePath), { + devEngines: { + packageManager: { + name: `npm`, + version: `bad`, + onFail: `warn`, + }, + }, + }); + + await expect(runCli(projectCwd, [`npm`, `--version`])).resolves.toMatchObject({ + exitCode: 0, + stderr: expect.stringContaining(`The value of devEngines.packageManager.version "bad" is not a valid semver range`), + stdout: `${config.definitions.npm.default.split(`+`, 1)[0]}\n`, + }); + }); +}); + it(`should expose its root to spawned processes`, async () => { await xfs.mktempPromise(async cwd => { await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), { From bdf6d1540fce27129dfbdb17c63463908409103e Mon Sep 17 00:00:00 2001 From: michkot <14879243+michkot@users.noreply.github.com> Date: Sat, 18 Apr 2026 00:07:00 +0200 Subject: [PATCH 02/12] test: reproduce devEngines manifest selection bug in Engine.findProjectSpec --- tests/main.test.ts | 89 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/tests/main.test.ts b/tests/main.test.ts index 49d395d4c..76a291e51 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -4,6 +4,7 @@ import process from 'node:process'; import {afterEach, beforeEach, describe, expect, it} from 'vitest'; import config from '../config.json'; +import {Engine} from '../sources/Engine'; import * as folderUtils from '../sources/folderUtils'; import {SupportedPackageManagerSet} from '../sources/types'; @@ -664,6 +665,94 @@ it(`should ignore a parent packageManager when a closer devEngines.packageManage }); }); +it(`should use the closest devEngines.packageManager field when parent has no spec in Engine.findProjectSpec`, async () => { + await xfs.mktempPromise(async cwd => { + const projectCwd = ppath.join(cwd, `foo` as PortablePath); + + await xfs.mkdirPromise(projectCwd, {recursive: true}); + + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), { + // empty package.json file + }); + await xfs.writeJsonPromise(ppath.join(projectCwd, `package.json` as Filename), { + devEngines: { + packageManager: { + name: `npm`, + version: `6.14.2`, + }, + }, + }); + + const engine = new Engine(); + await expect(engine.findProjectSpec(npath.fromPortablePath(projectCwd), { + name: `npm`, + reference: `9.9.9`, + })).resolves.toMatchObject({ + name: `npm`, + range: `6.14.2`, + }); + }); +}); + +it(`should use the closest devEngines.packageManager field over a parent packageManager in Engine.findProjectSpec`, async () => { + await xfs.mktempPromise(async cwd => { + const projectCwd = ppath.join(cwd, `foo` as PortablePath); + + await xfs.mkdirPromise(projectCwd, {recursive: true}); + + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), { + packageManager: `yarn@1.22.4`, + }); + await xfs.writeJsonPromise(ppath.join(projectCwd, `package.json` as Filename), { + devEngines: { + packageManager: { + name: `npm`, + version: `6.14.2`, + }, + }, + }); + + const engine = new Engine(); + await expect(engine.findProjectSpec(npath.fromPortablePath(projectCwd), { + name: `npm`, + reference: `9.9.9`, + })).resolves.toMatchObject({ + name: `npm`, + range: `6.14.2`, + }); + }); +}); + +it(`should ignore a parent packageManager when a closer devEngines.packageManager is invalid with onFail set to "warn" in Engine.findProjectSpec`, async () => { + await xfs.mktempPromise(async cwd => { + const projectCwd = ppath.join(cwd, `foo` as PortablePath); + + await xfs.mkdirPromise(projectCwd, {recursive: true}); + + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), { + packageManager: `yarn@1.22.4`, + }); + await xfs.writeJsonPromise(ppath.join(projectCwd, `package.json` as Filename), { + devEngines: { + packageManager: { + name: `npm`, + version: `bad`, + onFail: `warn`, + }, + }, + }); + + const engine = new Engine(); + await expect(engine.findProjectSpec(npath.fromPortablePath(projectCwd), { + name: `npm`, + reference: `9.9.9`, + })).resolves.toMatchObject({ + name: `npm`, + range: `9.9.9`, + }); + }); +}); + it(`should expose its root to spawned processes`, async () => { await xfs.mktempPromise(async cwd => { await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), { From dc96367ca291ce615ab1139dff2b98bc7e22d343 Mon Sep 17 00:00:00 2001 From: michkot <14879243+michkot@users.noreply.github.com> Date: Sat, 18 Apr 2026 01:23:03 +0200 Subject: [PATCH 03/12] fix: stop manifest search on devEngines package managers --- sources/Engine.ts | 2 +- sources/commands/Base.ts | 2 +- sources/specUtils.ts | 100 +++++++++++++++++++++++++++------------ tests/main.test.ts | 23 +++++++++ 4 files changed, 96 insertions(+), 31 deletions(-) diff --git a/sources/Engine.ts b/sources/Engine.ts index d93501596..0634bccf1 100644 --- a/sources/Engine.ts +++ b/sources/Engine.ts @@ -302,7 +302,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.sourceField}" field`); } } else { debugUtils.log(`Using ${spec.name}@${spec.range} as defined in project manifest ${result.target}`); diff --git a/sources/commands/Base.ts b/sources/commands/Base.ts index 950195427..e943449b0 100644 --- a/sources/commands/Base.ts +++ b/sources/commands/Base.ts @@ -19,7 +19,7 @@ export abstract class BaseCommand extends Command { 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`); default: { - return [lookup.range ?? lookup.getSpec()]; + return [lookup.devEnginesRange ?? lookup.getSpec()]; } } } diff --git a/sources/specUtils.ts b/sources/specUtils.ts index 8b73aacd2..4c0e40728 100644 --- a/sources/specUtils.ts +++ b/sources/specUtils.ts @@ -15,19 +15,32 @@ import type {LocalEnvFile} from './types'; const nodeModulesRegExp = /[\\/]node_modules[\\/](@[^\\/]*[\\/])?([^@\\/][^\\/]*)$/; -export function parseSpec(raw: unknown, source: string, {enforceExactVersion = true} = {}): Descriptor { +export function parseSpec(arg: string | ParsedPackageManager, source: string, {enforceExactVersion = true} = {}): Descriptor { + let raw: string; + let sourceField: PackageManagerSourceField | undefined; + + if (typeof arg === `object` && arg.sourceField !== undefined) { + raw = arg.rawPmSpec; + sourceField = arg.sourceField; + } else { + raw = arg as string; + sourceField = undefined; + } + + const maybeSourceFieldOf = sourceField ? `"${sourceField}" of ` : ``; + if (typeof raw !== `string`) - throw new UsageError(`Invalid package manager specification in ${source}; expected a string`); + throw new UsageError(`Invalid package manager specification in ${maybeSourceFieldOf}${source}; expected a string`); const atIndex = raw.indexOf(`@`); if (atIndex === -1 || atIndex === raw.length - 1) { if (enforceExactVersion) - throw new UsageError(`No version specified for ${raw} in "packageManager" of ${source}`); + throw new UsageError(`No version specified for ${raw} in ${maybeSourceFieldOf}${source}`); const name = atIndex === -1 ? raw : raw.slice(0, -1); if (!isSupportedPackageManager(name)) - throw new UsageError(`Unsupported package manager specification (${name})`); + throw new UsageError(`Unsupported package manager specification (${name}) in ${maybeSourceFieldOf}${source}`); return { name, range: `*`, @@ -40,13 +53,13 @@ export function parseSpec(raw: unknown, source: string, {enforceExactVersion = t const isURL = URL.canParse(range); if (!isURL) { if (enforceExactVersion && !semverValid(range)) - throw new UsageError(`Invalid package manager specification in ${source} (${raw}); expected a semver version${enforceExactVersion ? `` : `, range, or tag`}`); + throw new UsageError(`Invalid package manager specification in ${maybeSourceFieldOf}${source} (${raw}); expected a semver version${enforceExactVersion ? `` : `, range, or tag`}`); if (!isSupportedPackageManager(name)) { - throw new UsageError(`Unsupported package manager specification (${raw})`); + throw new UsageError(`Unsupported package manager specification (${raw}) in ${maybeSourceFieldOf}${source}`); } } else if (isSupportedPackageManager(name) && process.env.COREPACK_ENABLE_UNSAFE_CUSTOM_URLS !== `1`) { - 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 (${raw})`); + 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 (${raw}) in ${maybeSourceFieldOf}${source}`); } @@ -61,6 +74,20 @@ type CorepackPackageJSON = { devEngines?: {packageManager?: DevEngineDependency}; }; +type PackageManagerSourceField = `packageManager` | `devEngines.packageManager`; + +type ParsedPackageManager = { + sourceField: PackageManagerSourceField; + rawPmSpec: string; + devEnginesValues?: DevEngineDependency; +} & ({ + sourceField: `packageManager`; +} | { + sourceField: `devEngines.packageManager`; + devEnginesValues: DevEngineDependency; +} +); + interface DevEngineDependency { name: string; version: string; @@ -77,28 +104,32 @@ function warnOrThrow(errorMessage: string, onFail?: DevEngineDependency[`onFail` console.warn(`! Corepack validation warning: ${errorMessage}`); } } -function parsePackageJSON(packageJSONContent: CorepackPackageJSON) { +function parsePackageJSON(packageJSONContent: CorepackPackageJSON): ParsedPackageManager | undefined { const {packageManager: pm} = packageJSONContent; - if (packageJSONContent.devEngines?.packageManager != null) { + const resultFromPackageManager = pm + ? {sourceField: `packageManager`, rawPmSpec: pm} satisfies ParsedPackageManager + : undefined; + + if (packageJSONContent.devEngines?.packageManager) { 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 resultFromPackageManager; } if (Array.isArray(packageManager)) { console.warn(`! Corepack does not currently support array values for devEngines.packageManager`); - return pm; + return resultFromPackageManager; } 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 resultFromPackageManager; } 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 resultFromPackageManager; } debugUtils.log(`devEngines.packageManager defines that ${name}@${version} is the local package manager`); @@ -110,20 +141,25 @@ function parsePackageJSON(packageJSONContent: CorepackPackageJSON) { else if (version != null && !semverSatisfies(pm.slice(packageManager.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 { + ...resultFromPackageManager!, + devEnginesValues: packageManager, + }; } - - - return `${name}@${version ?? `*`}`; + return { + sourceField: `devEngines.packageManager`, + rawPmSpec: `${name}@${version ?? `*`}`, + devEnginesValues: packageManager, + }; } - return pm; + return resultFromPackageManager; } export async function setLocalPackageManager(cwd: string, info: PreparedPackageManagerInfo) { const lookup = await loadSpec(cwd); - const range = `range` in lookup && lookup.range; + const range = `devEnginesRange` in lookup && lookup.devEnginesRange; 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); @@ -151,8 +187,9 @@ interface FoundSpecResult { type: `Found`; target: string; getSpec: (options?: {enforceExactVersion?: boolean}) => Descriptor; - range?: Descriptor & {onFail?: DevEngineDependency[`onFail`]}; envFilePath?: string; + sourceField: PackageManagerSourceField; // source of the spec + devEnginesRange?: Descriptor & {onFail: Required[`onFail`]}; } export type LoadSpecResult = | {type: `NoProject`, target: string} @@ -170,7 +207,11 @@ export async function loadSpec(initialCwd: string): Promise { localEnv: LocalEnvFile; } | null = null; - while (nextCwd !== currCwd && (!selection || !selection.data.packageManager)) { + const selectionHasPmSpecified = (selection: {data: CorepackPackageJSON} | null) => { + return selection !== null && (selection.data.packageManager || selection.data.devEngines?.packageManager); + }; + + while (nextCwd !== currCwd && !selectionHasPmSpecified(selection)) { currCwd = nextCwd; nextCwd = path.dirname(currCwd); @@ -233,22 +274,23 @@ export async function loadSpec(initialCwd: string): Promise { process.env = selection.localEnv; } - const rawPmSpec = parsePackageJSON(selection.data); - if (typeof rawPmSpec === `undefined`) + const parsedPackageManager = parsePackageJSON(selection.data); + if (typeof parsedPackageManager === `undefined`) return {type: `NoSpec`, target: selection.manifestPath}; - debugUtils.log(`${selection.manifestPath} defines ${rawPmSpec} as local package manager`); + debugUtils.log(`${selection.manifestPath} defines ${parsedPackageManager.rawPmSpec} as local package manager via ${parsedPackageManager.sourceField}`); return { type: `Found`, target: selection.manifestPath, + sourceField: parsedPackageManager.sourceField, envFilePath, - range: selection.data.devEngines?.packageManager?.version && { - name: selection.data.devEngines.packageManager.name, - range: selection.data.devEngines.packageManager.version, - onFail: selection.data.devEngines.packageManager.onFail, + devEnginesRange: parsedPackageManager.devEnginesValues && { + name: parsedPackageManager.devEnginesValues.name, + range: parsedPackageManager.devEnginesValues.version, + onFail: parsedPackageManager.devEnginesValues.onFail ?? `error`, }, // 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(parsedPackageManager, path.relative(initialCwd, selection.manifestPath), {enforceExactVersion}), }; } diff --git a/tests/main.test.ts b/tests/main.test.ts index 76a291e51..5640b7eef 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -753,6 +753,29 @@ it(`should ignore a parent packageManager when a closer devEngines.packageManage }); }); +it(`should mention devEngines.packageManager when Engine.findProjectSpec rejects a mismatched package manager`, async () => { + await xfs.mktempPromise(async cwd => { + const projectCwd = ppath.join(cwd, `foo` as PortablePath); + + await xfs.mkdirPromise(projectCwd, {recursive: true}); + + await xfs.writeJsonPromise(ppath.join(projectCwd, `package.json` as Filename), { + devEngines: { + packageManager: { + name: `npm`, + version: `6.14.2`, + }, + }, + }); + + const engine = new Engine(); + await expect(engine.findProjectSpec(npath.fromPortablePath(projectCwd), { + name: `yarn`, + reference: `1.22.4`, + })).rejects.toThrow(`This project is configured to use npm because ${npath.fromPortablePath(ppath.join(projectCwd, `package.json` as Filename))} has a "devEngines.packageManager" field`); + }); +}); + it(`should expose its root to spawned processes`, async () => { await xfs.mktempPromise(async cwd => { await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), { From 1655735490ff3e65c9d56b5665cdae88da1af768 Mon Sep 17 00:00:00 2001 From: michkot <14879243+michkot@users.noreply.github.com> Date: Sat, 18 Apr 2026 01:23:23 +0200 Subject: [PATCH 04/12] test: reject empty and null packageManager values --- sources/specUtils.ts | 19 ++++++++-- tests/main.test.ts | 90 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 4 deletions(-) diff --git a/sources/specUtils.ts b/sources/specUtils.ts index 4c0e40728..effacf7c1 100644 --- a/sources/specUtils.ts +++ b/sources/specUtils.ts @@ -106,11 +106,15 @@ function warnOrThrow(errorMessage: string, onFail?: DevEngineDependency[`onFail` } function parsePackageJSON(packageJSONContent: CorepackPackageJSON): ParsedPackageManager | undefined { const {packageManager: pm} = packageJSONContent; - const resultFromPackageManager = pm + + const resultFromPackageManager = pm !== undefined ? {sourceField: `packageManager`, rawPmSpec: pm} satisfies ParsedPackageManager : undefined; - if (packageJSONContent.devEngines?.packageManager) { + if (pm === `` || pm === null) + return resultFromPackageManager; // short-circuit with defined, but invalid "packageManager" values + + if (packageJSONContent.devEngines?.packageManager !== undefined) { const {packageManager} = packageJSONContent.devEngines; if (typeof packageManager !== `object`) { @@ -134,7 +138,7 @@ function parsePackageJSON(packageJSONContent: CorepackPackageJSON): ParsedPackag debugUtils.log(`devEngines.packageManager defines that ${name}@${version} is the local package manager`); - if (pm) { + if (pm !== undefined) { 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); @@ -196,6 +200,13 @@ export type LoadSpecResult = | {type: `NoSpec`, target: string} | FoundSpecResult; +// We walk up the directory tree to support workspace-style projects whose +// package manager may be declared in a higher-level manifest. The search +// stops at the nearest package.json that defines either package manager +// field, even if that value is invalid. The tradeoff is that if the nearest +// package.json does not define any package manager field, then an upper-level +// package.json can dictate which package manager gets used even when that +// higher-level manifest is unrelated to the current project. export async function loadSpec(initialCwd: string): Promise { let nextCwd = initialCwd; let currCwd = ``; @@ -208,7 +219,7 @@ export async function loadSpec(initialCwd: string): Promise { } | null = null; const selectionHasPmSpecified = (selection: {data: CorepackPackageJSON} | null) => { - return selection !== null && (selection.data.packageManager || selection.data.devEngines?.packageManager); + return selection !== null && (selection.data.packageManager !== undefined || selection.data.devEngines?.packageManager !== undefined); }; while (nextCwd !== currCwd && !selectionHasPmSpecified(selection)) { diff --git a/tests/main.test.ts b/tests/main.test.ts index 5640b7eef..833d3b1b2 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -776,6 +776,96 @@ it(`should mention devEngines.packageManager when Engine.findProjectSpec rejects }); }); +it(`should treat packageManager empty string as an invalid selected value in Engine.findProjectSpec`, async () => { + await xfs.mktempPromise(async cwd => { + const projectCwd = ppath.join(cwd, `foo` as PortablePath); + + await xfs.mkdirPromise(projectCwd, {recursive: true}); + + await xfs.writeJsonPromise(ppath.join(projectCwd, `package.json` as Filename), { + packageManager: ``, + devEngines: { + packageManager: { + name: `npm`, + version: `6.14.2`, + }, + }, + }); + + const engine = new Engine(); + await expect(engine.findProjectSpec(npath.fromPortablePath(projectCwd), { + name: `npm`, + reference: `9.9.9`, + })).rejects.toThrow(`No version specified for in "packageManager" of package.json`); + }); +}); + +it(`should stop at a child packageManager empty string instead of falling back to a parent manifest in Engine.findProjectSpec`, async () => { + await xfs.mktempPromise(async cwd => { + const projectCwd = ppath.join(cwd, `foo` as PortablePath); + + await xfs.mkdirPromise(projectCwd, {recursive: true}); + + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), { + packageManager: `yarn@1.22.4`, + }); + await xfs.writeJsonPromise(ppath.join(projectCwd, `package.json` as Filename), { + packageManager: ``, + }); + + const engine = new Engine(); + await expect(engine.findProjectSpec(npath.fromPortablePath(projectCwd), { + name: `yarn`, + reference: `1.22.4`, + })).rejects.toThrow(`No version specified for in "packageManager" of package.json`); + }); +}); + +it(`should treat packageManager null as an invalid selected value in Engine.findProjectSpec`, async () => { + await xfs.mktempPromise(async cwd => { + const projectCwd = ppath.join(cwd, `foo` as PortablePath); + + await xfs.mkdirPromise(projectCwd, {recursive: true}); + + await xfs.writeJsonPromise(ppath.join(projectCwd, `package.json` as Filename), { + packageManager: null, + devEngines: { + packageManager: { + name: `npm`, + version: `6.14.2`, + }, + }, + }); + + const engine = new Engine(); + await expect(engine.findProjectSpec(npath.fromPortablePath(projectCwd), { + name: `npm`, + reference: `9.9.9`, + })).rejects.toThrow(`Invalid package manager specification in "packageManager" of package.json; expected a string`); + }); +}); + +it(`should stop at a child packageManager null instead of falling back to a parent manifest in Engine.findProjectSpec`, async () => { + await xfs.mktempPromise(async cwd => { + const projectCwd = ppath.join(cwd, `foo` as PortablePath); + + await xfs.mkdirPromise(projectCwd, {recursive: true}); + + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), { + packageManager: `yarn@1.22.4`, + }); + await xfs.writeJsonPromise(ppath.join(projectCwd, `package.json` as Filename), { + packageManager: null, + }); + + const engine = new Engine(); + await expect(engine.findProjectSpec(npath.fromPortablePath(projectCwd), { + name: `yarn`, + reference: `1.22.4`, + })).rejects.toThrow(`Invalid package manager specification in "packageManager" of package.json; expected a string`); + }); +}); + it(`should expose its root to spawned processes`, async () => { await xfs.mktempPromise(async cwd => { await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), { From d663b1deea06b5984340e2eba30152d35029abe6 Mon Sep 17 00:00:00 2001 From: michkot <14879243+michkot@users.noreply.github.com> Date: Sat, 18 Apr 2026 00:11:08 +0200 Subject: [PATCH 05/12] test: keep only mixed-field CLI repro coverage --- tests/main.test.ts | 55 ---------------------------------------------- 1 file changed, 55 deletions(-) diff --git a/tests/main.test.ts b/tests/main.test.ts index 833d3b1b2..e9f445033 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -583,33 +583,6 @@ it(`should use the closest matching packageManager field`, async () => { }); }); -it(`should use the closest matching devEngines.packageManager field when parent has no spec`, async () => { - await xfs.mktempPromise(async cwd => { - const projectCwd = ppath.join(cwd, `foo` as PortablePath); - - await xfs.mkdirPromise(projectCwd, {recursive: true}); - - await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as PortablePath), { - // empty package.json file - }); - - await xfs.writeJsonPromise(ppath.join(projectCwd, `package.json` as PortablePath), { - devEngines: { - packageManager: { - name: `npm`, - version: `6.14.2`, - }, - }, - }); - - await expect(runCli(projectCwd, [`npm`, `--version`])).resolves.toMatchObject({ - exitCode: 0, - stderr: ``, - stdout: `6.14.2\n`, - }); - }); -}); - it(`should use the closest matching devEngines.packageManager field over a parent packageManager field`, async () => { await xfs.mktempPromise(async cwd => { const projectCwd = ppath.join(cwd, `foo` as PortablePath); @@ -637,34 +610,6 @@ it(`should use the closest matching devEngines.packageManager field over a paren }); }); -it(`should ignore a parent packageManager when a closer devEngines.packageManager is invalid but non-fatal`, async () => { - await xfs.mktempPromise(async cwd => { - const projectCwd = ppath.join(cwd, `foo` as PortablePath); - - await xfs.mkdirPromise(projectCwd, {recursive: true}); - - await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as PortablePath), { - packageManager: `yarn@1.22.4`, - }); - - await xfs.writeJsonPromise(ppath.join(projectCwd, `package.json` as PortablePath), { - devEngines: { - packageManager: { - name: `npm`, - version: `bad`, - onFail: `warn`, - }, - }, - }); - - await expect(runCli(projectCwd, [`npm`, `--version`])).resolves.toMatchObject({ - exitCode: 0, - stderr: expect.stringContaining(`The value of devEngines.packageManager.version "bad" is not a valid semver range`), - stdout: `${config.definitions.npm.default.split(`+`, 1)[0]}\n`, - }); - }); -}); - it(`should use the closest devEngines.packageManager field when parent has no spec in Engine.findProjectSpec`, async () => { await xfs.mktempPromise(async cwd => { const projectCwd = ppath.join(cwd, `foo` as PortablePath); From 4d85054701c735a355b9817c2a399200a8d244ae Mon Sep 17 00:00:00 2001 From: michkot <14879243+michkot@users.noreply.github.com> Date: Sat, 18 Apr 2026 05:27:38 +0200 Subject: [PATCH 06/12] fix: let devEngines mismatches with onFail: warn fall back to the requested package manager --- sources/Engine.ts | 9 +++- sources/specUtils.ts | 20 ++++++--- tests/main.test.ts | 104 +++++++++++++++++++++++++++++++++++++++---- 3 files changed, 118 insertions(+), 15 deletions(-) diff --git a/sources/Engine.ts b/sources/Engine.ts index 0634bccf1..0c875a4af 100644 --- a/sources/Engine.ts +++ b/sources/Engine.ts @@ -295,10 +295,17 @@ export class Engine { case `Found`: { const spec = result.getSpec({enforceExactVersion: !binaryVersion}); if (spec.name !== locator.name) { - if (transparent) { + const devEnginesSayMismatchIsNotError = result.sourceField === `devEngines.packageManager` + && result.devEnginesRange !== undefined + && result.devEnginesRange.onFail !== `error`; + + if (transparent || devEnginesSayMismatchIsNotError) { if (typeof locator.reference === `function`) fallbackDescriptor.range = await locator.reference(); + if (devEnginesSayMismatchIsNotError && result.devEnginesRange!.onFail === `warn`) + console.warn(`! Corepack validation warning: Using ${fallbackDescriptor.name} as requested (@${fallbackDescriptor.range}) because ${result.target} defines "devEngines.packageManager" with mismatched ${spec.name}@${spec.range} and onFail: warn.`); + debugUtils.log(`Falling back to ${fallbackDescriptor.name}@${fallbackDescriptor.range} in a ${spec.name}@${spec.range} project`); return fallbackDescriptor; } else { diff --git a/sources/specUtils.ts b/sources/specUtils.ts index effacf7c1..233c3e44d 100644 --- a/sources/specUtils.ts +++ b/sources/specUtils.ts @@ -91,14 +91,24 @@ type ParsedPackageManager = { interface DevEngineDependency { name: string; version: string; - onFail?: `ignore` | `warn` | `error`; + onFail?: `ignore` | `warn` | `error` | `download`; } -function warnOrThrow(errorMessage: string, onFail?: DevEngineDependency[`onFail`]) { + +function normalizeOnFail(onFail?: DevEngineDependency[`onFail`]) { switch (onFail) { + case undefined: + case `download`: + return `error`; + default: + return onFail; + } +} + +function warnOrThrow(errorMessage: string, onFail?: DevEngineDependency[`onFail`]) { + switch (normalizeOnFail(onFail)) { case `ignore`: break; case `error`: - case undefined: throw new UsageError(errorMessage); default: console.warn(`! Corepack validation warning: ${errorMessage}`); @@ -193,7 +203,7 @@ interface FoundSpecResult { getSpec: (options?: {enforceExactVersion?: boolean}) => Descriptor; envFilePath?: string; sourceField: PackageManagerSourceField; // source of the spec - devEnginesRange?: Descriptor & {onFail: Required[`onFail`]}; + devEnginesRange?: Descriptor & {onFail: `ignore` | `warn` | `error`}; } export type LoadSpecResult = | {type: `NoProject`, target: string} @@ -299,7 +309,7 @@ export async function loadSpec(initialCwd: string): Promise { devEnginesRange: parsedPackageManager.devEnginesValues && { name: parsedPackageManager.devEnginesValues.name, range: parsedPackageManager.devEnginesValues.version, - onFail: parsedPackageManager.devEnginesValues.onFail ?? `error`, + onFail: normalizeOnFail(parsedPackageManager.devEnginesValues.onFail), }, // Lazy-loading it so we do not throw errors on commands that do not need valid spec. getSpec: ({enforceExactVersion = true} = {}) => parseSpec(parsedPackageManager, path.relative(initialCwd, selection.manifestPath), {enforceExactVersion}), diff --git a/tests/main.test.ts b/tests/main.test.ts index e9f445033..12f05f1bd 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -1,14 +1,14 @@ -import {Filename, ppath, xfs, npath, PortablePath} from '@yarnpkg/fslib'; -import os from 'node:os'; -import process from 'node:process'; -import {afterEach, beforeEach, describe, expect, it} from 'vitest'; +import {Filename, ppath, xfs, npath, PortablePath} from '@yarnpkg/fslib'; +import os from 'node:os'; +import process from 'node:process'; +import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'; -import config from '../config.json'; -import {Engine} from '../sources/Engine'; -import * as folderUtils from '../sources/folderUtils'; -import {SupportedPackageManagerSet} from '../sources/types'; +import config from '../config.json'; +import {Engine} from '../sources/Engine'; +import * as folderUtils from '../sources/folderUtils'; +import {SupportedPackageManagerSet} from '../sources/types'; -import {runCli} from './_runCli'; +import {runCli} from './_runCli'; beforeEach(async () => { @@ -721,6 +721,92 @@ it(`should mention devEngines.packageManager when Engine.findProjectSpec rejects }); }); +it(`should fall back to the requested package manager when only devEngines.packageManager mismatches with onFail set to "ignore" in Engine.findProjectSpec`, async () => { + await xfs.mktempPromise(async cwd => { + const projectCwd = ppath.join(cwd, `foo` as PortablePath); + + await xfs.mkdirPromise(projectCwd, {recursive: true}); + + await xfs.writeJsonPromise(ppath.join(projectCwd, `package.json` as Filename), { + devEngines: { + packageManager: { + name: `npm`, + version: `6.14.2`, + onFail: `ignore`, + }, + }, + }); + + const engine = new Engine(); + await expect(engine.findProjectSpec(npath.fromPortablePath(projectCwd), { + name: `yarn`, + reference: `1.22.4`, + })).resolves.toMatchObject({ + name: `yarn`, + range: `1.22.4`, + }); + }); +}); + +it(`should fall back to the requested package manager and warn when only devEngines.packageManager mismatches with onFail set to "warn" in Engine.findProjectSpec`, async () => { + await xfs.mktempPromise(async cwd => { + const projectCwd = ppath.join(cwd, `foo` as PortablePath); + + await xfs.mkdirPromise(projectCwd, {recursive: true}); + + await xfs.writeJsonPromise(ppath.join(projectCwd, `package.json` as Filename), { + devEngines: { + packageManager: { + name: `npm`, + version: `6.14.2`, + onFail: `warn`, + }, + }, + }); + + const warn = vi.spyOn(console, `warn`).mockImplementation(() => {}); + + try { + const engine = new Engine(); + await expect(engine.findProjectSpec(npath.fromPortablePath(projectCwd), { + name: `yarn`, + reference: `1.22.4`, + })).resolves.toMatchObject({ + name: `yarn`, + range: `1.22.4`, + }); + + expect(warn).toHaveBeenCalledWith(`! Corepack validation warning: Using yarn as requested (@1.22.4) because ${npath.fromPortablePath(ppath.join(projectCwd, `package.json` as Filename))} defines "devEngines.packageManager" with mismatched npm@6.14.2 and onFail: warn.`); + } finally { + warn.mockRestore(); + } + }); +}); + +it(`should reject when only devEngines.packageManager mismatches with onFail set to "download" in Engine.findProjectSpec`, async () => { + await xfs.mktempPromise(async cwd => { + const projectCwd = ppath.join(cwd, `foo` as PortablePath); + + await xfs.mkdirPromise(projectCwd, {recursive: true}); + + await xfs.writeJsonPromise(ppath.join(projectCwd, `package.json` as Filename), { + devEngines: { + packageManager: { + name: `npm`, + version: `6.14.2`, + onFail: `download`, + }, + }, + }); + + const engine = new Engine(); + await expect(engine.findProjectSpec(npath.fromPortablePath(projectCwd), { + name: `yarn`, + reference: `1.22.4`, + })).rejects.toThrow(`This project is configured to use npm because ${npath.fromPortablePath(ppath.join(projectCwd, `package.json` as Filename))} has a "devEngines.packageManager" field`); + }); +}); + it(`should treat packageManager empty string as an invalid selected value in Engine.findProjectSpec`, async () => { await xfs.mktempPromise(async cwd => { const projectCwd = ppath.join(cwd, `foo` as PortablePath); From ace9662caa032cf95836537ed28ae98af7ad072b Mon Sep 17 00:00:00 2001 From: michkot <14879243+michkot@users.noreply.github.com> Date: Sat, 18 Apr 2026 05:25:57 +0200 Subject: [PATCH 07/12] fix: report selected package.json path in invalid packageManager errors --- sources/specUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sources/specUtils.ts b/sources/specUtils.ts index 233c3e44d..063eba453 100644 --- a/sources/specUtils.ts +++ b/sources/specUtils.ts @@ -255,7 +255,7 @@ export async function loadSpec(initialCwd: string): Promise { } catch {} if (typeof data !== `object` || data === null) - throw new UsageError(`Invalid package.json in ${path.relative(initialCwd, manifestPath)}`); + throw new UsageError(`Could not parse ${path.relative(initialCwd, manifestPath)} as JSON.`); let localEnv: LocalEnvFile; const envFilePath = path.resolve(currCwd, process.env.COREPACK_ENV_FILE ?? `.corepack.env`); From dc4fb732375b3fbb0bf1ef20d568a0fed6aca9d6 Mon Sep 17 00:00:00 2001 From: michkot <14879243+michkot@users.noreply.github.com> Date: Sat, 18 Apr 2026 05:06:59 +0200 Subject: [PATCH 08/12] docs: clarify findProjectSpec comment about project package-manager enforcement --- sources/Engine.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/sources/Engine.ts b/sources/Engine.ts index 0c875a4af..aba5a131d 100644 --- a/sources/Engine.ts +++ b/sources/Engine.ts @@ -229,19 +229,20 @@ export class Engine { } /** - * Locates the active project's package manager specification. + * Locates the package manager specification from nearest package.json having one. * * If the specification exists but doesn't match the active package manager, * an error is thrown to prevent users from using the wrong package manager, * which would lead to inconsistent project layouts. * - * If the project doesn't include a specification file, we just assume that - * whatever the user uses is exactly what they want to use. Since the version - * isn't specified, we fallback on known good versions. + * If none of the package.json files have specification, + * or a package.json had specification which was invalid but allowed failure, + * we just assume that whatever the user uses is exactly what they want to use. + * Since the version isn't specified, we fallback on known good versions. * - * Finally, if the project doesn't exist at all, we ask the user whether they - * want to create one in the current project. If they do, we initialize a new - * project using the default package managers, and configure it so that we + * Finally, if the package.json doesn't exist at all, we ask the user whether they + * want to create one in the current working directory. If they do, we initialize a new + * package.json 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 { From 24aa06774717a21d07178acb3ab09e36935c2849 Mon Sep 17 00:00:00 2001 From: michkot <14879243+michkot@users.noreply.github.com> Date: Sat, 18 Apr 2026 05:10:05 +0200 Subject: [PATCH 09/12] docs: correct findProjectSpec comment about fallback behavior --- sources/Engine.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/sources/Engine.ts b/sources/Engine.ts index aba5a131d..840768b66 100644 --- a/sources/Engine.ts +++ b/sources/Engine.ts @@ -240,10 +240,9 @@ export class Engine { * we just assume that whatever the user uses is exactly what they want to use. * Since the version isn't specified, we fallback on known good versions. * - * Finally, if the package.json doesn't exist at all, we ask the user whether they - * want to create one in the current working directory. If they do, we initialize a new - * package.json using the default package managers, and configure it so that we - * don't need to ask again in the future. + * Finally, if the package.json doesn't exist at all, + * we just assume that whatever the user uses is exactly what they want to use. + * Since the version isn't specified, we fallback on known good versions. */ 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) From 5e3446365892160157040e1146e935977d2f9e8b Mon Sep 17 00:00:00 2001 From: michkot <14879243+michkot@users.noreply.github.com> Date: Sat, 18 Apr 2026 05:28:55 +0200 Subject: [PATCH 10/12] refactor: remove redundant loop from Engine.findProjectSpec All code paths already had an explicit control-flow exit via return or throw, so the loop did not affect behavior. --- sources/Engine.ts | 92 +++++++++++++++++++++++------------------------ 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/sources/Engine.ts b/sources/Engine.ts index 840768b66..0cde06934 100644 --- a/sources/Engine.ts +++ b/sources/Engine.ts @@ -258,66 +258,66 @@ export class Engine { if (process.env.COREPACK_ENABLE_STRICT === `0`) transparent = true; - while (true) { - const result = await specUtils.loadSpec(initialCwd); + const result = await specUtils.loadSpec(initialCwd); - switch (result.type) { - case `NoProject`: { - if (typeof locator.reference === `function`) - fallbackDescriptor.range = await locator.reference(); + switch (result.type) { + case `NoProject`: { + if (typeof locator.reference === `function`) + fallbackDescriptor.range = await locator.reference(); - debugUtils.log(`Falling back to ${fallbackDescriptor.name}@${fallbackDescriptor.range} as no project manifest were found`); - return fallbackDescriptor; - } + debugUtils.log(`Falling back to ${fallbackDescriptor.name}@${fallbackDescriptor.range} as no project manifest were found`); + return fallbackDescriptor; + } - case `NoSpec`: { - if (typeof locator.reference === `function`) - fallbackDescriptor.range = await locator.reference(); + case `NoSpec`: { + if (typeof locator.reference === `function`) + fallbackDescriptor.range = await locator.reference(); - if (process.env.COREPACK_ENABLE_AUTO_PIN === `1`) { - 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`); + if (process.env.COREPACK_ENABLE_AUTO_PIN === `1`) { + 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`); - const installSpec = await this.ensurePackageManager(resolved); + const installSpec = await this.ensurePackageManager(resolved); - console.error(`! The local project doesn't define a 'packageManager' field. Corepack will now add one referencing ${installSpec.locator.name}@${installSpec.locator.reference}.`); - console.error(`! For more details about this field, consult the documentation at https://nodejs.org/api/packages.html#packagemanager`); - console.error(); + console.error(`! The local project doesn't define a 'packageManager' field. Corepack will now add one referencing ${installSpec.locator.name}@${installSpec.locator.reference}.`); + console.error(`! For more details about this field, consult the documentation at https://nodejs.org/api/packages.html#packagemanager`); + console.error(); - await specUtils.setLocalPackageManager(path.dirname(result.target), installSpec); - } - - debugUtils.log(`Falling back to ${fallbackDescriptor.name}@${fallbackDescriptor.range} in the absence of "packageManager" field in ${result.target}`); - return fallbackDescriptor; + await specUtils.setLocalPackageManager(path.dirname(result.target), installSpec); } - case `Found`: { - const spec = result.getSpec({enforceExactVersion: !binaryVersion}); - if (spec.name !== locator.name) { - const devEnginesSayMismatchIsNotError = result.sourceField === `devEngines.packageManager` - && result.devEnginesRange !== undefined - && result.devEnginesRange.onFail !== `error`; - - if (transparent || devEnginesSayMismatchIsNotError) { - if (typeof locator.reference === `function`) - fallbackDescriptor.range = await locator.reference(); - - if (devEnginesSayMismatchIsNotError && result.devEnginesRange!.onFail === `warn`) - console.warn(`! Corepack validation warning: Using ${fallbackDescriptor.name} as requested (@${fallbackDescriptor.range}) because ${result.target} defines "devEngines.packageManager" with mismatched ${spec.name}@${spec.range} and onFail: warn.`); - - 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 "${result.sourceField}" field`); - } + debugUtils.log(`Falling back to ${fallbackDescriptor.name}@${fallbackDescriptor.range} in the absence of "packageManager" field in ${result.target}`); + return fallbackDescriptor; + } + + case `Found`: { + const spec = result.getSpec({enforceExactVersion: !binaryVersion}); + if (spec.name !== locator.name) { + const devEnginesSayMismatchIsNotError = result.sourceField === `devEngines.packageManager` + && result.devEnginesRange !== undefined + && result.devEnginesRange.onFail !== `error`; + + if (transparent || devEnginesSayMismatchIsNotError) { + if (typeof locator.reference === `function`) + fallbackDescriptor.range = await locator.reference(); + + if (devEnginesSayMismatchIsNotError && result.devEnginesRange!.onFail === `warn`) + console.warn(`! Corepack validation warning: Using ${fallbackDescriptor.name} as requested (@${fallbackDescriptor.range}) because ${result.target} defines "devEngines.packageManager" with mismatched ${spec.name}@${spec.range} and onFail: warn.`); + + debugUtils.log(`Falling back to ${fallbackDescriptor.name}@${fallbackDescriptor.range} in a ${spec.name}@${spec.range} project`); + return fallbackDescriptor; } else { - debugUtils.log(`Using ${spec.name}@${spec.range} as defined in project manifest ${result.target}`); - return spec; + throw new UsageError(`This project is configured to use ${spec.name} because ${result.target} has a "${result.sourceField}" field`); } + } else { + debugUtils.log(`Using ${spec.name}@${spec.range} as defined in project manifest ${result.target}`); + return spec; } } } + + throw new Error(`Assertion failed: Unsupported loadSpec result type`); } async executePackageManagerRequest({packageManager, binaryName, binaryVersion}: PackageManagerRequest, {cwd, args}: {cwd: string, args: Array}): Promise { From 79ff5dc5120881105a3c9024d30ed1f06d912ebf Mon Sep 17 00:00:00 2001 From: michkot <14879243+michkot@users.noreply.github.com> Date: Sat, 18 Apr 2026 05:30:58 +0200 Subject: [PATCH 11/12] refactor: invert parsePackageJSON condition to reduce nesting --- sources/specUtils.ts | 66 ++++++++++++++++++++++---------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/sources/specUtils.ts b/sources/specUtils.ts index 063eba453..8fb7f5003 100644 --- a/sources/specUtils.ts +++ b/sources/specUtils.ts @@ -124,50 +124,50 @@ function parsePackageJSON(packageJSONContent: CorepackPackageJSON): ParsedPackag if (pm === `` || pm === null) return resultFromPackageManager; // short-circuit with defined, but invalid "packageManager" values - if (packageJSONContent.devEngines?.packageManager !== undefined) { - const {packageManager} = packageJSONContent.devEngines; + if (packageJSONContent.devEngines?.packageManager === undefined) + return resultFromPackageManager; - 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 resultFromPackageManager; - } - if (Array.isArray(packageManager)) { - console.warn(`! Corepack does not currently support array values for devEngines.packageManager`); - return resultFromPackageManager; - } + const {packageManager: packageManager} = packageJSONContent.devEngines; - 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 resultFromPackageManager; - } - 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 resultFromPackageManager; - } + 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 resultFromPackageManager; + } + if (Array.isArray(packageManager)) { + console.warn(`! Corepack does not currently support array values for devEngines.packageManager`); + return resultFromPackageManager; + } - debugUtils.log(`devEngines.packageManager defines that ${name}@${version} is the local package manager`); + 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 resultFromPackageManager; + } + 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 resultFromPackageManager; + } - if (pm !== undefined) { - 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); + debugUtils.log(`devEngines.packageManager defines that ${name}@${version} is the local package manager`); - else if (version != null && !semverSatisfies(pm.slice(packageManager.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); + if (pm !== undefined) { + 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)) + 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 { - ...resultFromPackageManager!, - devEnginesValues: packageManager, - }; - } return { - sourceField: `devEngines.packageManager`, - rawPmSpec: `${name}@${version ?? `*`}`, + ...resultFromPackageManager!, devEnginesValues: packageManager, }; } - return resultFromPackageManager; + return { + sourceField: `devEngines.packageManager`, + rawPmSpec: `${name}@${version ?? `*`}`, + devEnginesValues: packageManager, + }; } export async function setLocalPackageManager(cwd: string, info: PreparedPackageManagerInfo) { From 1cc5eb0c95483e572a049528c6a768cf848fe620 Mon Sep 17 00:00:00 2001 From: michkot <14879243+michkot@users.noreply.github.com> Date: Sat, 18 Apr 2026 05:31:21 +0200 Subject: [PATCH 12/12] refactor: rename parsePackageJSON locals for clarity --- sources/specUtils.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/sources/specUtils.ts b/sources/specUtils.ts index 8fb7f5003..ec0427af1 100644 --- a/sources/specUtils.ts +++ b/sources/specUtils.ts @@ -127,18 +127,18 @@ function parsePackageJSON(packageJSONContent: CorepackPackageJSON): ParsedPackag if (packageJSONContent.devEngines?.packageManager === undefined) return resultFromPackageManager; - const {packageManager: packageManager} = packageJSONContent.devEngines; + const {packageManager: devEnginesPackageManager} = 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.`); + if (typeof devEnginesPackageManager !== `object`) { + console.warn(`! Corepack only supports objects as valid value for devEngines.packageManager. The current value (${JSON.stringify(devEnginesPackageManager)}) will be ignored.`); return resultFromPackageManager; } - if (Array.isArray(packageManager)) { + if (Array.isArray(devEnginesPackageManager)) { console.warn(`! Corepack does not currently support array values for devEngines.packageManager`); return resultFromPackageManager; } - const {name, version, onFail} = packageManager; + const {name, version, onFail} = devEnginesPackageManager; if (typeof name !== `string` || name.includes(`@`)) { warnOrThrow(`The value of devEngines.packageManager.name ${JSON.stringify(name)} is not a supported string value`, onFail); return resultFromPackageManager; @@ -154,19 +154,19 @@ function parsePackageJSON(packageJSONContent: CorepackPackageJSON): ParsedPackag 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(devEnginesPackageManager.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 { ...resultFromPackageManager!, - devEnginesValues: packageManager, + devEnginesValues: devEnginesPackageManager, }; } return { sourceField: `devEngines.packageManager`, rawPmSpec: `${name}@${version ?? `*`}`, - devEnginesValues: packageManager, + devEnginesValues: devEnginesPackageManager, }; }