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 package manager, and to not update the Last Known Good version when it
downloads a new version of the same major line. 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 - `COREPACK_ENABLE_AUTO_PIN` can be set to `0` to prevent Corepack from
updating the `packageManager` field when it detects that the local package updating the `packageManager` field when it detects that the local package
doesn't list it. In general we recommend to always list a `packageManager` 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`); 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) {
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); 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` const content = lookup.type !== `NoProject`
? await fs.promises.readFile(lookup.target, `utf8`) ? await fs.promises.readFile((lookup as FoundSpecResult).envFilePath ?? lookup.target, `utf8`)
: ``; : ``;
const {data, indent} = nodeUtils.readPackageJson(content); 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 previousPackageManager = data.packageManager ?? (range ? `${range.name}@${range.range}` : `unknown`); const lineEndIndex = content.indexOf(`\n`, index);
data.packageManager = `${info.locator.name}@${info.locator.reference}`;
const newContent = nodeUtils.normalizeLineEndings(content, `${JSON.stringify(data, null, indent)}\n`); previousPackageManager = content.slice(index, lineEndIndex === -1 ? undefined : lineEndIndex);
await fs.promises.writeFile(lookup.target, newContent, `utf8`); newContent = `${content.slice(0, index)}\n${envKey}=${info.locator.reference}\n${lineEndIndex === -1 ? `` : content.slice(lineEndIndex)}`;
} else {
const {data, indent} = nodeUtils.readPackageJson(content);
previousPackageManager = data.packageManager ?? (range ? `${range.name}@${range.range}` : `unknown`);
data.packageManager = `${info.locator.name}@${info.locator.reference}`;
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 { return {
previousPackageManager, previousPackageManager,

View File

@ -1,5 +1,6 @@
import {ppath, xfs, npath} from '@yarnpkg/fslib'; import {ppath, xfs, npath} from '@yarnpkg/fslib';
import process from 'node:process'; import process from 'node:process';
import {parseEnv} from 'node:util';
import {describe, beforeEach, it, expect} from 'vitest'; import {describe, beforeEach, it, expect} from 'vitest';
import {runCli} from './_runCli'; 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 {ppath, xfs, npath} from '@yarnpkg/fslib';
import process from 'node:process'; import process from 'node:process';
import {parseEnv} from 'node:util';
import {describe, beforeEach, it, expect} from 'vitest'; import {describe, beforeEach, it, expect} from 'vitest';
import {runCli} from './_runCli'; 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 () => { it(`should create a package.json if absent`, async () => {
await xfs.mktempPromise(async cwd => { await xfs.mktempPromise(async cwd => {
await expect(runCli(cwd, [`use`, `yarn@1.22.4`])).resolves.toMatchObject({ 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: ``,
});
})));
});
}); });