Skip to content

Commit f75e1a7

Browse files
committed
.corepack.env as a lock file
1 parent 12bd990 commit f75e1a7

File tree

3 files changed

+119
-7
lines changed

3 files changed

+119
-7
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,10 @@ same major line. Should you need to upgrade to a new major, use an explicit
286286
package manager, and to not update the Last Known Good version when it
287287
downloads a new version of the same major line.
288288

289+
- `COREPACK_DEV_ENGINES_${UPPER_CASE_PACKAGE_MANAGER_NAME}` can be set to give
290+
Corepack a specific version matching the range defined in `package.json`'s
291+
`devEngines.packageManager` field.
292+
289293
- `COREPACK_ENABLE_AUTO_PIN` can be set to `0` to prevent Corepack from
290294
updating the `packageManager` field when it detects that the local package
291295
doesn't list it. In general we recommend to always list a `packageManager`

sources/specUtils.ts

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,17 @@ function parsePackageJSON(packageJSONContent: CorepackPackageJSON) {
102102

103103
debugUtils.log(`devEngines.packageManager defines that ${name}@${version} is the local package manager`);
104104

105+
const localEnvKey = `COREPACK_DEV_ENGINES_${packageManager.name.toUpperCase()}`;
106+
const localEnvVersion = process.env[localEnvKey];
107+
if (localEnvVersion) {
108+
debugUtils.log(`Environment defines that ${name}@${localEnvVersion} is the local package manager`);
109+
110+
if (!semverSatisfies(localEnvVersion, version))
111+
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);
112+
113+
return `${name}@${localEnvVersion}`;
114+
}
115+
105116
const {packageManager: pm} = packageJSONContent;
106117
if (pm) {
107118
if (!pm.startsWith(`${name}@`))
@@ -124,16 +135,33 @@ export async function setLocalPackageManager(cwd: string, info: PreparedPackageM
124135
const lookup = await loadSpec(cwd);
125136

126137
const content = lookup.type !== `NoProject`
127-
? await fs.promises.readFile(lookup.target, `utf8`)
138+
? await fs.promises.readFile((lookup as FoundSpecResult).envFilePath ?? lookup.target, `utf8`)
128139
: ``;
129140

130-
const {data, indent} = nodeUtils.readPackageJson(content);
141+
let previousPackageManager: string;
142+
let newContent: string;
143+
if ((lookup as FoundSpecResult).envFilePath) {
144+
const envKey = `COREPACK_DEV_ENGINES_${(lookup as FoundSpecResult).spec.name.toUpperCase()}`;
145+
const index = content.lastIndexOf(`\n${envKey}=`) + 1;
146+
147+
if (index === 0 && !content.startsWith(`${envKey}=`))
148+
throw new Error(`INTERNAL ASSERTION ERROR: missing expected ${envKey} in .corepack.env`);
131149

132-
const previousPackageManager = data.packageManager ?? `unknown`;
133-
data.packageManager = `${info.locator.name}@${info.locator.reference}`;
150+
const lineEndIndex = content.indexOf(`\n`, index);
151+
152+
previousPackageManager = content.slice(index, lineEndIndex === -1 ? undefined : lineEndIndex);
153+
newContent = `${content.slice(0, index)}\n${envKey}=${info.locator.reference}\n${lineEndIndex === -1 ? `` : content.slice(lineEndIndex)}`;
154+
} else {
155+
const {data, indent} = nodeUtils.readPackageJson(content);
156+
157+
previousPackageManager = data.packageManager ?? `unknown`;
158+
data.packageManager = `${info.locator.name}@${info.locator.reference}`;
159+
160+
newContent = `${JSON.stringify(data, null, indent)}\n`;
161+
}
134162

135-
const newContent = nodeUtils.normalizeLineEndings(content, `${JSON.stringify(data, null, indent)}\n`);
136-
await fs.promises.writeFile(lookup.target, newContent, `utf8`);
163+
newContent = nodeUtils.normalizeLineEndings(content, newContent);
164+
await fs.promises.writeFile((lookup as FoundSpecResult).envFilePath ?? lookup.target, newContent, `utf8`);
137165

138166
return {
139167
previousPackageManager,

tests/Use.test.ts

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {ppath, xfs, npath} from '@yarnpkg/fslib';
22
import process from 'node:process';
3+
import {parseEnv} from 'node:util';
34
import {describe, beforeEach, it, expect} from 'vitest';
45

56
import {runCli} from './_runCli';
@@ -11,7 +12,7 @@ beforeEach(async () => {
1112
});
1213

1314
describe(`UseCommand`, () => {
14-
it(`should set the package manager in the current project`, async () => {
15+
it(`should update the "packageManager" field in the current project`, async () => {
1516
await xfs.mktempPromise(async cwd => {
1617
await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), {
1718
packageManager: `[email protected]`,
@@ -32,6 +33,85 @@ describe(`UseCommand`, () => {
3233
});
3334
});
3435

36+
it(`should update .corepack.env if present and contains definition for pm version`, async t => {
37+
// Skip that test on Node.js 18.x as it lacks support for .env files.
38+
if (process.version.startsWith(`v18.`)) t.skip();
39+
40+
await Promise.all([
41+
`COREPACK_DEV_ENGINES_YARN=1.1.0\n`,
42+
`\nCOREPACK_DEV_ENGINES_YARN=1.1.0\n`,
43+
`COREPACK_DEV_ENGINES_YARN=1.1.0`,
44+
`\nCOREPACK_DEV_ENGINES_YARN=1.1.0`,
45+
`FOO=bar\nCOREPACK_DEV_ENGINES_YARN=1.1.0\n`,
46+
`FOO=bar\nCOREPACK_DEV_ENGINES_YARN=1.1.0`,
47+
].map(originalEnv => xfs.mktempPromise(async cwd => {
48+
const pJSONContent = {
49+
devEngines: {packageManager: {name: `yarn`, version: `1.x`}},
50+
license: `MIT`,
51+
};
52+
await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), pJSONContent);
53+
await xfs.writeFilePromise(ppath.join(cwd, `.corepack.env`), originalEnv);
54+
55+
await expect(runCli(cwd, [`use`, `[email protected]`])).resolves.toMatchObject({
56+
exitCode: 0,
57+
stdout: expect.stringContaining(`Installing [email protected] in the project...`),
58+
stderr: ``,
59+
});
60+
61+
try {
62+
await expect(xfs.readFilePromise(ppath.join(cwd, `.corepack.env`), `utf-8`).then(parseEnv)).resolves.toMatchObject({
63+
COREPACK_DEV_ENGINES_YARN: `1.22.4+sha512.a1833b862fe52169bd6c2a033045a07df5bc6a23595c259e675fed1b2d035ab37abe6ce309720abb6636d68f03615054b6292dc0a70da31c8697fda228b50d18`,
64+
});
65+
} catch (cause) {
66+
throw new Error(JSON.stringify(originalEnv), {cause});
67+
}
68+
// It should not have touched package.json.
69+
await expect(xfs.readJsonPromise(ppath.join(cwd, `package.json`))).resolves.toStrictEqual(pJSONContent);
70+
71+
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
72+
exitCode: 0,
73+
stdout: `1.22.4\n`,
74+
});
75+
})));
76+
});
77+
78+
it(`should update .other.env if present`, async t => {
79+
// Skip that test on Node.js 18.x as it lacks support for .env files.
80+
if (process.version.startsWith(`v18.`)) t.skip();
81+
82+
await Promise.all([
83+
`COREPACK_DEV_ENGINES_YARN=1.1.0\n`,
84+
`\nCOREPACK_DEV_ENGINES_YARN=1.1.0\n`,
85+
`COREPACK_DEV_ENGINES_YARN=1.1.0`,
86+
`\nCOREPACK_DEV_ENGINES_YARN=1.1.0`,
87+
`FOO=bar\nCOREPACK_DEV_ENGINES_YARN=1.1.0\n`,
88+
`FOO=bar\nCOREPACK_DEV_ENGINES_YARN=1.1.0`,
89+
].map(originalEnv => xfs.mktempPromise(async cwd => {
90+
await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), {
91+
devEngines: {packageManager: {name: `yarn`, version: `1.x`}},
92+
});
93+
await xfs.writeFilePromise(ppath.join(cwd, `.other.env`), `COREPACK_DEV_ENGINES_YARN=1.0.0\n`);
94+
95+
process.env.COREPACK_ENV_FILE = `.other.env`;
96+
await expect(runCli(cwd, [`use`, `[email protected]`])).resolves.toMatchObject({
97+
exitCode: 0,
98+
});
99+
100+
try {
101+
await expect(xfs.readFilePromise(ppath.join(cwd, `.other.env`), `utf-8`).then(parseEnv)).resolves.toMatchObject({
102+
COREPACK_DEV_ENGINES_YARN: `1.22.4+sha512.a1833b862fe52169bd6c2a033045a07df5bc6a23595c259e675fed1b2d035ab37abe6ce309720abb6636d68f03615054b6292dc0a70da31c8697fda228b50d18`,
103+
});
104+
} catch (cause) {
105+
throw new Error(JSON.stringify(originalEnv), {cause});
106+
}
107+
108+
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
109+
exitCode: 0,
110+
stdout: `1.22.4\n`,
111+
});
112+
})));
113+
});
114+
35115
it(`should create a package.json if absent`, async () => {
36116
await xfs.mktempPromise(async cwd => {
37117
await expect(runCli(cwd, [`use`, `[email protected]`])).resolves.toMatchObject({

0 commit comments

Comments
 (0)