diff --git a/sources/specUtils.ts b/sources/specUtils.ts index edd5c7e91..82c143597 100644 --- a/sources/specUtils.ts +++ b/sources/specUtils.ts @@ -77,47 +77,51 @@ function warnOrThrow(errorMessage: string, onFail?: DevEngineDependency[`onFail` console.warn(`! Corepack validation warning: ${errorMessage}`); } } -function parsePackageJSON(packageJSONContent: CorepackPackageJSON) { - 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; +function parsePackageJSON({devEngines, packageManager}: CorepackPackageJSON) { + const spec = { + packageManager, + enforceExactVersion: true, + }; + + if (devEngines?.packageManager != null) { + const {packageManager: pm} = devEngines; + + if (typeof pm !== `object`) { + console.warn(`! Corepack only supports objects as valid value for devEngines.packageManager. The current value (${JSON.stringify(pm)}) will be ignored.`); + return spec; } - if (Array.isArray(packageManager)) { + if (Array.isArray(pm)) { console.warn(`! Corepack does not currently support array values for devEngines.packageManager`); - return pm; + return spec; } - const {name, version, onFail} = packageManager; + const {name, version, onFail} = pm; 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 spec; } 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 spec; } debugUtils.log(`devEngines.packageManager defines that ${name}@${version} is the local package manager`); - if (pm) { - 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 pm; + if (packageManager) { + if (!packageManager.startsWith?.(`${name}@`)) + warnOrThrow(`"packageManager" field is set to ${JSON.stringify(packageManager)} which does not match the "devEngines.packageManager" field set to ${JSON.stringify(name)}`, onFail); + else if (version != null && !semverSatisfies(packageManager.slice(pm.name.length + 1), version)) + warnOrThrow(`"packageManager" field is set to ${JSON.stringify(packageManager)} which does not match the value defined in "devEngines.packageManager" for ${JSON.stringify(name)} of ${JSON.stringify(version)}`, onFail); + return spec; } - - return `${name}@${version ?? `*`}`; + return { + enforceExactVersion: semverValid(version), + packageManager: `${name}@${version ?? `*`}`, + }; } - return pm; + return spec; } export async function setLocalPackageManager(cwd: string, info: PreparedPackageManagerInfo) { @@ -233,11 +237,11 @@ export async function loadSpec(initialCwd: string): Promise { process.env = selection.localEnv; } - const rawPmSpec = parsePackageJSON(selection.data); - if (typeof rawPmSpec === `undefined`) + const {enforceExactVersion, packageManager} = parsePackageJSON(selection.data); + if (typeof packageManager === `undefined`) return {type: `NoSpec`, target: selection.manifestPath}; - debugUtils.log(`${selection.manifestPath} defines ${rawPmSpec} as local package manager`); + debugUtils.log(`${selection.manifestPath} defines ${packageManager} as local package manager`); return { type: `Found`, @@ -249,6 +253,6 @@ export async function loadSpec(initialCwd: string): Promise { onFail: selection.data.devEngines.packageManager.onFail, }, // Lazy-loading it so we do not throw errors on commands that do not need valid spec. - getSpec: () => parseSpec(rawPmSpec, path.relative(initialCwd, selection.manifestPath)), + getSpec: () => parseSpec(packageManager, path.relative(initialCwd, selection.manifestPath), {enforceExactVersion}), }; } diff --git a/tests/main.test.ts b/tests/main.test.ts index 80f117673..6839a9c9b 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -377,8 +377,8 @@ it(`should use hash from "packageManager" even when "devEngines" defines a diffe }); }); -describe(`should accept range in devEngines only if a specific version is provided`, () => { - it(`either in package.json#packageManager field`, async () => { +describe(`should accept range in devEngines only if`, () => { + it(`either is fully defined in package.json#packageManager field`, async () => { await xfs.mktempPromise(async cwd => { await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as PortablePath), { devEngines: { @@ -387,18 +387,22 @@ describe(`should accept range in devEngines only if a specific version is provid version: `6.x`, }, }, + packageManager: `pnpm@6.6.2+sha224.eb5c0acad3b0f40ecdaa2db9aa5a73134ad256e17e22d1419a2ab073`, }); await expect(runCli(cwd, [`pnpm`, `--version`])).resolves.toMatchObject({ - exitCode: 1, - stderr: `Invalid package manager specification in package.json (pnpm@6.x); expected a semver version\n`, - stdout: ``, + exitCode: 0, + stderr: ``, + stdout: `6.6.2\n`, }); + }); + }); + it(`has no packageManager.version at all`, async () => { + await xfs.mktempPromise(async cwd => { await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as PortablePath), { devEngines: { packageManager: { name: `pnpm`, - version: `6.x`, }, }, packageManager: `pnpm@6.6.2+sha224.eb5c0acad3b0f40ecdaa2db9aa5a73134ad256e17e22d1419a2ab073`, @@ -408,16 +412,20 @@ describe(`should accept range in devEngines only if a specific version is provid stderr: ``, stdout: `6.6.2\n`, }); + }); + }); - // No version should also work + it(`does not use package.json#packageManager at all`, async () => { + await xfs.mktempPromise(async cwd => { await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as PortablePath), { devEngines: { packageManager: { name: `pnpm`, + version: `~6.6.0`, }, }, - packageManager: `pnpm@6.6.2+sha224.eb5c0acad3b0f40ecdaa2db9aa5a73134ad256e17e22d1419a2ab073`, }); + await expect(runCli(cwd, [`pnpm`, `--version`])).resolves.toMatchObject({ exitCode: 0, stderr: ``,