This commit is contained in:
Antoine du Hamel 2025-05-02 20:08:15 +02:00 committed by GitHub
commit cb7fca0d08
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 196 additions and 6 deletions

View File

@ -286,6 +286,10 @@ same major line. Should you need to upgrade to a new major, use an explicit
package manager, and to not update the Last Known Good version when it
downloads a new version of the same major line.
- `COREPACK_DEV_ENGINES_${UPPER_CASE_PACKAGE_MANAGER_NAME}` can be set to give
Corepack a specific version matching the range defined in `package.json`'s
`devEngines.packageManager` field.
- `COREPACK_ENABLE_AUTO_PIN` can be set to `0` to prevent Corepack from
updating the `packageManager` field when it detects that the local package
doesn't list it. In general we recommend to always list a `packageManager`

View File

@ -103,6 +103,17 @@ function parsePackageJSON(packageJSONContent: CorepackPackageJSON) {
debugUtils.log(`devEngines.packageManager defines that ${name}@${version} is the local package manager`);
const localEnvKey = `COREPACK_DEV_ENGINES_${packageManager.name.toUpperCase()}`;
const localEnvVersion = process.env[localEnvKey];
if (localEnvVersion) {
debugUtils.log(`Environment defines that ${name}@${localEnvVersion} is the local package manager`);
if (!semverSatisfies(localEnvVersion, version))
warnOrThrow(`"${localEnvKey}" environment variable is set to ${JSON.stringify(localEnvVersion)} which does not match the value defined in "devEngines.packageManager" for ${JSON.stringify(name)} of ${JSON.stringify(version)}`, onFail);
return `${name}@${localEnvVersion}`;
}
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);
@ -131,16 +142,33 @@ export async function setLocalPackageManager(cwd: string, info: PreparedPackageM
}
const content = lookup.type !== `NoProject`
? await fs.promises.readFile(lookup.target, `utf8`)
? await fs.promises.readFile((lookup as FoundSpecResult).envFilePath ?? lookup.target, `utf8`)
: ``;
let previousPackageManager: string;
let newContent: string;
if ((lookup as FoundSpecResult).envFilePath) {
const {name} = range || (lookup as FoundSpecResult).getSpec();
const envKey = `COREPACK_DEV_ENGINES_${name.toUpperCase()}`;
const index = content.lastIndexOf(`\n${envKey}=`) + 1;
if (index === 0 && !content.startsWith(`${envKey}=`))
throw new Error(`INTERNAL ASSERTION ERROR: missing expected ${envKey} in .corepack.env`);
const lineEndIndex = content.indexOf(`\n`, index);
previousPackageManager = content.slice(index, lineEndIndex === -1 ? undefined : lineEndIndex);
newContent = `${content.slice(0, index)}\n${envKey}=${info.locator.reference}\n${lineEndIndex === -1 ? `` : content.slice(lineEndIndex)}`;
} else {
const {data, indent} = nodeUtils.readPackageJson(content);
const previousPackageManager = data.packageManager ?? (range ? `${range.name}@${range.range}` : `unknown`);
previousPackageManager = data.packageManager ?? (range ? `${range.name}@${range.range}` : `unknown`);
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`);
newContent = `${JSON.stringify(data, null, indent)}\n`;
}
newContent = nodeUtils.normalizeLineEndings(content, newContent);
await fs.promises.writeFile((lookup as FoundSpecResult).envFilePath ?? lookup.target, newContent, `utf8`);
return {
previousPackageManager,

View File

@ -1,5 +1,6 @@
import {ppath, xfs, npath} from '@yarnpkg/fslib';
import process from 'node:process';
import {parseEnv} from 'node:util';
import {describe, beforeEach, it, expect} from 'vitest';
import {runCli} from './_runCli';
@ -133,4 +134,42 @@ describe(`UpCommand`, () => {
});
});
});
it(`should update the ".corepack.env" file from the current project`, async t => {
// Skip that test on Node.js 18.x as it lacks support for .env files.
if (process.version.startsWith(`v18.`)) t.skip();
await Promise.all([
`COREPACK_DEV_ENGINES_YARN=1.1.0\n`,
`\nCOREPACK_DEV_ENGINES_YARN=1.1.0\n`,
`COREPACK_DEV_ENGINES_YARN=1.1.0`,
`\nCOREPACK_DEV_ENGINES_YARN=1.1.0`,
`FOO=bar\nCOREPACK_DEV_ENGINES_YARN=1.1.0\n`,
`FOO=bar\nCOREPACK_DEV_ENGINES_YARN=1.1.0`,
].map(originalEnv => xfs.mktempPromise(async cwd => {
await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), {
devEngines: {packageManager: {name: `yarn`, version: `1.x || 2.x`}},
});
await xfs.writeFilePromise(ppath.join(cwd, `.corepack.env`), originalEnv);
await expect(runCli(cwd, [`up`])).resolves.toMatchObject({
exitCode: 0,
stderr: ``,
stdout: expect.stringMatching(/^Installing yarn@2\.4\.3 in the project\.\.\.\n\n/),
});
try {
await expect(xfs.readFilePromise(ppath.join(cwd, `.corepack.env`), `utf-8`).then(parseEnv)).resolves.toMatchObject({
COREPACK_DEV_ENGINES_YARN: `2.4.3+sha512.8dd9fedc5451829619e526c56f42609ad88ae4776d9d3f9456d578ac085115c0c2f0fb02bb7d57fd2e1b6e1ac96efba35e80a20a056668f61c96934f67694fd0`,
});
} catch (cause) {
throw new Error(JSON.stringify(originalEnv), {cause});
}
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
exitCode: 0,
stdout: `2.4.3\n`,
stderr: ``,
});
})));
});
});

View File

@ -1,5 +1,6 @@
import {ppath, xfs, npath} from '@yarnpkg/fslib';
import process from 'node:process';
import {parseEnv} from 'node:util';
import {describe, beforeEach, it, expect} from 'vitest';
import {runCli} from './_runCli';
@ -115,6 +116,85 @@ describe(`UseCommand`, () => {
});
});
it(`should update .corepack.env if present and contains definition for pm version`, async t => {
// Skip that test on Node.js 18.x as it lacks support for .env files.
if (process.version.startsWith(`v18.`)) t.skip();
await Promise.all([
`COREPACK_DEV_ENGINES_YARN=1.1.0\n`,
`\nCOREPACK_DEV_ENGINES_YARN=1.1.0\n`,
`COREPACK_DEV_ENGINES_YARN=1.1.0`,
`\nCOREPACK_DEV_ENGINES_YARN=1.1.0`,
`FOO=bar\nCOREPACK_DEV_ENGINES_YARN=1.1.0\n`,
`FOO=bar\nCOREPACK_DEV_ENGINES_YARN=1.1.0`,
].map(originalEnv => xfs.mktempPromise(async cwd => {
const pJSONContent = {
devEngines: {packageManager: {name: `yarn`, version: `1.x`}},
license: `MIT`,
};
await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), pJSONContent);
await xfs.writeFilePromise(ppath.join(cwd, `.corepack.env`), originalEnv);
await expect(runCli(cwd, [`use`, `yarn@1.22.4`])).resolves.toMatchObject({
exitCode: 0,
stdout: expect.stringContaining(`Installing yarn@1.22.4 in the project...`),
stderr: ``,
});
try {
await expect(xfs.readFilePromise(ppath.join(cwd, `.corepack.env`), `utf-8`).then(parseEnv)).resolves.toMatchObject({
COREPACK_DEV_ENGINES_YARN: `1.22.4+sha512.a1833b862fe52169bd6c2a033045a07df5bc6a23595c259e675fed1b2d035ab37abe6ce309720abb6636d68f03615054b6292dc0a70da31c8697fda228b50d18`,
});
} catch (cause) {
throw new Error(JSON.stringify(originalEnv), {cause});
}
// It should not have touched package.json.
await expect(xfs.readJsonPromise(ppath.join(cwd, `package.json`))).resolves.toStrictEqual(pJSONContent);
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
exitCode: 0,
stdout: `1.22.4\n`,
});
})));
});
it(`should update .other.env if present`, async t => {
// Skip that test on Node.js 18.x as it lacks support for .env files.
if (process.version.startsWith(`v18.`)) t.skip();
await Promise.all([
`COREPACK_DEV_ENGINES_YARN=1.1.0\n`,
`\nCOREPACK_DEV_ENGINES_YARN=1.1.0\n`,
`COREPACK_DEV_ENGINES_YARN=1.1.0`,
`\nCOREPACK_DEV_ENGINES_YARN=1.1.0`,
`FOO=bar\nCOREPACK_DEV_ENGINES_YARN=1.1.0\n`,
`FOO=bar\nCOREPACK_DEV_ENGINES_YARN=1.1.0`,
].map(originalEnv => xfs.mktempPromise(async cwd => {
await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), {
devEngines: {packageManager: {name: `yarn`, version: `1.x`}},
});
await xfs.writeFilePromise(ppath.join(cwd, `.other.env`), `COREPACK_DEV_ENGINES_YARN=1.0.0\n`);
process.env.COREPACK_ENV_FILE = `.other.env`;
await expect(runCli(cwd, [`use`, `yarn@1.22.4`])).resolves.toMatchObject({
exitCode: 0,
});
try {
await expect(xfs.readFilePromise(ppath.join(cwd, `.other.env`), `utf-8`).then(parseEnv)).resolves.toMatchObject({
COREPACK_DEV_ENGINES_YARN: `1.22.4+sha512.a1833b862fe52169bd6c2a033045a07df5bc6a23595c259e675fed1b2d035ab37abe6ce309720abb6636d68f03615054b6292dc0a70da31c8697fda228b50d18`,
});
} catch (cause) {
throw new Error(JSON.stringify(originalEnv), {cause});
}
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
exitCode: 0,
stdout: `1.22.4\n`,
});
})));
});
it(`should create a package.json if absent`, async () => {
await xfs.mktempPromise(async cwd => {
await expect(runCli(cwd, [`use`, `yarn@1.22.4`])).resolves.toMatchObject({
@ -193,4 +273,43 @@ describe(`UseCommand`, () => {
});
}
});
it(`should update the ".corepack.env" file from the current project`, async t => {
// Skip that test on Node.js 18.x as it lacks support for .env files.
if (process.version.startsWith(`v18.`)) t.skip();
await Promise.all([
`COREPACK_DEV_ENGINES_YARN=2.1.0\n`,
`\nCOREPACK_DEV_ENGINES_YARN=2.1.0\n`,
`COREPACK_DEV_ENGINES_YARN=2.1.0`,
`\nCOREPACK_DEV_ENGINES_YARN=2.1.0`,
`FOO=bar\nCOREPACK_DEV_ENGINES_YARN=2.1.0\n`,
`FOO=bar\nCOREPACK_DEV_ENGINES_YARN=2.1.0`,
].map(originalEnv => xfs.mktempPromise(async cwd => {
await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), {
devEngines: {packageManager: {name: `yarn`, version: `1.x || 2.x`}},
license: `MIT`, // To avoid Yarn warning.
});
await xfs.writeFilePromise(ppath.join(cwd, `.corepack.env`), originalEnv);
await expect(runCli(cwd, [`use`, `yarn@1.22.4`])).resolves.toMatchObject({
exitCode: 0,
stderr: ``,
stdout: expect.stringMatching(/^Installing yarn@1\.22\.4 in the project\.\.\.\n\n/),
});
try {
await expect(xfs.readFilePromise(ppath.join(cwd, `.corepack.env`), `utf-8`).then(parseEnv)).resolves.toMatchObject({
COREPACK_DEV_ENGINES_YARN: `1.22.4+sha512.a1833b862fe52169bd6c2a033045a07df5bc6a23595c259e675fed1b2d035ab37abe6ce309720abb6636d68f03615054b6292dc0a70da31c8697fda228b50d18`,
});
} catch (cause) {
throw new Error(JSON.stringify(originalEnv), {cause});
}
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
exitCode: 0,
stdout: `1.22.4\n`,
stderr: ``,
});
})));
});
});