diff --git a/sources/specUtils.ts b/sources/specUtils.ts index 8b73aacd2..c35e82806 100644 --- a/sources/specUtils.ts +++ b/sources/specUtils.ts @@ -124,11 +124,6 @@ export async function setLocalPackageManager(cwd: string, info: PreparedPackageM const lookup = await loadSpec(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 content = lookup.type !== `NoProject` ? await fs.promises.readFile(lookup.target, `utf8`) @@ -137,7 +132,18 @@ export async function setLocalPackageManager(cwd: string, info: PreparedPackageM const {data, indent} = nodeUtils.readPackageJson(content); const previousPackageManager = data.packageManager ?? (range ? `${range.name}@${range.range}` : `unknown`); - data.packageManager = `${info.locator.name}@${info.locator.reference}`; + + // When the project relies solely on an exact `devEngines.packageManager` + // version (i.e. there's no `packageManager` field), update that field in + // place rather than adding a conflicting `packageManager` entry (see #874). + if (range && data.packageManager == null && range.name === info.locator.name && semverValid(range.range)) { + data.devEngines.packageManager.version = info.locator.reference; + } else { + if (range && (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); + + data.packageManager = `${info.locator.name}@${info.locator.reference}`; + } const newContent = nodeUtils.normalizeLineEndings(content, `${JSON.stringify(data, null, indent)}\n`); await fs.promises.writeFile(lookup.target, newContent, `utf8`); diff --git a/tests/Use.test.ts b/tests/Use.test.ts index 978c2e3a8..4579571e7 100644 --- a/tests/Use.test.ts +++ b/tests/Use.test.ts @@ -77,6 +77,37 @@ describe(`UseCommand`, () => { }); }); + it(`with an exact version in 'devEngines.packageManager' and no 'packageManager' field`, async () => { + await xfs.mktempPromise(async cwd => { + process.env.NO_COLOR = `1`; + await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), { + devEngines: {packageManager: {name: `yarn`, version: `2.1.0`}}, + license: `MIT`, + }); + + // Should update the pinned devEngines version rather than error out + // when the requested version doesn't match the current one (#874): + await expect(runCli(cwd, [`use`, `yarn@2.4.3`])).resolves.toMatchObject({ + exitCode: 0, + stderr: ``, + stdout: expect.stringMatching(/^Installing yarn@2\.4\.3 in the project\.\.\.\n\n/), + }); + + const data = await xfs.readJsonPromise(ppath.join(cwd, `package.json`)); + expect(data).toMatchObject({ + devEngines: {packageManager: {name: `yarn`, version: `2.4.3+sha512.8dd9fedc5451829619e526c56f42609ad88ae4776d9d3f9456d578ac085115c0c2f0fb02bb7d57fd2e1b6e1ac96efba35e80a20a056668f61c96934f67694fd0`}}, + }); + // The `packageManager` field should not be added: + expect(data).not.toHaveProperty(`packageManager`); + + await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({ + exitCode: 0, + stdout: `2.4.3\n`, + stderr: ``, + }); + }); + }); + it(`with 'devEngines.packageManager' and 'packageManager' fields`, async () => { await xfs.mktempPromise(async cwd => { process.env.NO_COLOR = `1`;