Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: let .corepack.env be a lock file #668

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
40 changes: 34 additions & 6 deletions sources/specUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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`)
: ``;

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`);
data.packageManager = `${info.locator.name}@${info.locator.reference}`;
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);

previousPackageManager = data.packageManager ?? (range ? `${range.name}@${range.range}` : `unknown`);
data.packageManager = `${info.locator.name}@${info.locator.reference}`;

newContent = `${JSON.stringify(data, null, indent)}\n`;
}

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

return {
previousPackageManager,
Expand Down
39 changes: 39 additions & 0 deletions tests/Up.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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: ``,
});
})));
});
});
119 changes: 119 additions & 0 deletions tests/Use.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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`, `[email protected]`])).resolves.toMatchObject({
exitCode: 0,
stdout: expect.stringContaining(`Installing [email protected] 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`, `[email protected]`])).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`, `[email protected]`])).resolves.toMatchObject({
Expand Down Expand Up @@ -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`, `[email protected]`])).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: ``,
});
})));
});
});