diff --git a/README.md b/README.md index fea1e2c8f..4c9bac20f 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,35 @@ use in the archive). } ``` +#### `devEngines.packageManager` + +When a `devEngines.packageManager` field is defined, and is an object containing +a `"name"` field (can also optionally contain `version` and `onFail` fields), +Corepack will use it to validate you're using a compatible package manager. + +Depending on the value of `devEngines.packageManager.onFail`: + +- if set to `ignore`, Corepack won't print any warning or error. +- if unset or set to `error`, Corepack will throw an error in case of a mismatch. +- if set to `warn` or some other value, Corepack will print a warning in case + of mismatch. + +If the top-level `packageManager` field is missing, Corepack will use the +package manager defined in `devEngines.packageManager` – in which case you must +provide a specific version in `devEngines.packageManager.version`, ideally with +a hash, as explained in the previous section: + +```json +{ + "devEngines":{ + "packageManager": { + "name": "yarn", + "version": "3.2.3+sha224.953c8233f7a92884eee2de69a1b92d1f2ec1655e66d08071ba9a02fa" + } + } +} +``` + ## Known Good Releases When running Corepack within projects that don't list a supported package @@ -246,6 +275,7 @@ it. Unlike `corepack use` this command doesn't take a package manager name nor a version range, as it will always select the latest available version from the +range specified in `devEngines.packageManager.version`, or fallback to the same major line. Should you need to upgrade to a new major, use an explicit `corepack use {name}@latest` call (or simply `corepack use {name}`). @@ -256,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` @@ -267,6 +301,7 @@ same major line. Should you need to upgrade to a new major, use an explicit set to `1` to have the URL shown. By default, when Corepack is called explicitly (e.g. `corepack pnpm …`), it is set to `0`; when Corepack is called implicitly (e.g. `pnpm …`), it is set to `1`. + The default value cannot be overridden in a `.corepack.env` file. When standard input is a TTY and no CI environment is detected, Corepack will ask for user input before starting the download. @@ -292,6 +327,14 @@ same major line. Should you need to upgrade to a new major, use an explicit project. This means that it will always use the system-wide package manager regardless of what is being specified in the project's `packageManager` field. +- `COREPACK_ENV_FILE` can be set to `0` to request Corepack to not attempt to + load `.corepack.env`; it can be set to a path to specify a different env file. + Only keys that start with `COREPACK_` and are not in the exception list + (`COREPACK_ENABLE_DOWNLOAD_PROMPT` and `COREPACK_ENV_FILE` are ignored) + will be taken into account. + For Node.js 18.x users, this setting has no effect as that version doesn't + support parsing of `.env` files. + - `COREPACK_HOME` can be set in order to define where Corepack should install the package managers. By default it is set to `%LOCALAPPDATA%\node\corepack` on Windows, and to `$HOME/.cache/node/corepack` everywhere else. diff --git a/sources/commands/Base.ts b/sources/commands/Base.ts index ef7c0e4c8..ca0550129 100644 --- a/sources/commands/Base.ts +++ b/sources/commands/Base.ts @@ -19,7 +19,7 @@ export abstract class BaseCommand extends Command { throw new UsageError(`The local project doesn't feature a 'packageManager' field - please explicit the package manager to pack, or update the manifest to reference it`); default: { - return [lookup.spec]; + return [lookup.range ?? lookup.spec]; } } } diff --git a/sources/npmRegistryUtils.ts b/sources/npmRegistryUtils.ts index 963241755..04274321c 100644 --- a/sources/npmRegistryUtils.ts +++ b/sources/npmRegistryUtils.ts @@ -38,6 +38,8 @@ export function verifySignature({signatures, integrity, packageName, version}: { packageName: string; version: string; }) { + if (signatures == null) throw new Error(`No compatible signature found in package metadata for ${packageName}@${version}`); + const {npm: keys} = process.env.COREPACK_INTEGRITY_KEYS ? JSON.parse(process.env.COREPACK_INTEGRITY_KEYS) as typeof defaultConfig.keys : defaultConfig.keys; diff --git a/sources/specUtils.ts b/sources/specUtils.ts index a75b10ffd..d71b49e44 100644 --- a/sources/specUtils.ts +++ b/sources/specUtils.ts @@ -1,13 +1,17 @@ -import {UsageError} from 'clipanion'; -import fs from 'fs'; -import path from 'path'; -import semverValid from 'semver/functions/valid'; +import {UsageError} from 'clipanion'; +import fs from 'fs'; +import path from 'path'; +import semverSatisfies from 'semver/functions/satisfies'; +import semverValid from 'semver/functions/valid'; +import semverValidRange from 'semver/ranges/valid'; +import {parseEnv} from 'util'; -import {PreparedPackageManagerInfo} from './Engine'; -import * as debugUtils from './debugUtils'; -import {NodeError} from './nodeUtils'; -import * as nodeUtils from './nodeUtils'; -import {Descriptor, isSupportedPackageManager} from './types'; +import type {PreparedPackageManagerInfo} from './Engine'; +import * as debugUtils from './debugUtils'; +import type {NodeError} from './nodeUtils'; +import * as nodeUtils from './nodeUtils'; +import {isSupportedPackageManager} from './types'; +import type {LocalEnvFile, Descriptor} from './types'; const nodeModulesRegExp = /[\\/]node_modules[\\/](@[^\\/]*[\\/])?([^@\\/][^\\/]*)$/; @@ -52,38 +56,133 @@ export function parseSpec(raw: unknown, source: string, {enforceExactVersion = t }; } +type CorepackPackageJSON = { + packageManager?: string; + devEngines?: { packageManager?: DevEngineDependency }; +}; + +interface DevEngineDependency { + name: string; + version: string; + onFail?: 'ignore' | 'warn' | 'error'; +} +function warnOrThrow(errorMessage: string, onFail?: DevEngineDependency['onFail']) { + switch (onFail) { + case `ignore`: + break; + case `error`: + case undefined: + throw new UsageError(errorMessage); + default: + console.warn(`! Corepack validation warning: ${errorMessage}`); + } +} +function parsePackageJSON(packageJSONContent: CorepackPackageJSON) { + if (packageJSONContent.devEngines?.packageManager != null) { + const {packageManager} = packageJSONContent.devEngines; + + if (typeof packageManager !== `object`) { + console.warn(`! Corepack only supports objects as valid value for devEngines.packageManager. The current value (${JSON.stringify(packageManager)}) will be ignored.`); + return packageJSONContent.packageManager; + } + if (Array.isArray(packageManager)) { + console.warn(`! Corepack does not currently support array values for devEngines.packageManager`); + return packageJSONContent.packageManager; + } + + const {name, version, onFail} = packageManager; + if (typeof name !== `string` || name.includes(`@`)) { + warnOrThrow(`The value of devEngines.packageManager.name ${JSON.stringify(name)} is not a supported string value`, onFail); + return packageJSONContent.packageManager; + } + if (version != null && (typeof version !== `string` || !semverValidRange(version))) { + warnOrThrow(`The value of devEngines.packageManager.version ${JSON.stringify(version)} is not a valid semver range`, onFail); + return packageJSONContent.packageManager; + } + + 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}`; + } + + const {packageManager: pm} = packageJSONContent; + 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); + + else if (version != null && !semverSatisfies(pm.slice(packageManager.name.length + 1), version)) + warnOrThrow(`"packageManager" field is set to ${JSON.stringify(pm)} which does not match the value defined in "devEngines.packageManager" for ${JSON.stringify(name)} of ${JSON.stringify(version)}`, onFail); + + return pm; + } + + + return `${name}@${version ?? `*`}`; + } + + return packageJSONContent.packageManager; +} + export async function setLocalPackageManager(cwd: string, info: PreparedPackageManagerInfo) { - const lookup = await loadSpec(cwd); + const lookup = await loadSpec(cwd, true); 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 && (lookup as FoundSpecResult).range) { + const envKey = `COREPACK_DEV_ENGINES_${(lookup as FoundSpecResult).range!.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); - const previousPackageManager = data.packageManager ?? `unknown`; - data.packageManager = `${info.locator.name}@${info.locator.reference}`; + 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 newContent = nodeUtils.normalizeLineEndings(content, `${JSON.stringify(data, null, indent)}\n`); - await fs.promises.writeFile(lookup.target, newContent, `utf8`); + previousPackageManager = data.packageManager ?? `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 { previousPackageManager, }; } -export type LoadSpecResult = +type FoundSpecResult = {type: `Found`, target: string, spec: SkipSpecParsing extends true ? undefined : Descriptor, range?: Descriptor, envFilePath?: string}; +export type LoadSpecResult = | {type: `NoProject`, target: string} | {type: `NoSpec`, target: string} - | {type: `Found`, target: string, spec: Descriptor}; + | FoundSpecResult; -export async function loadSpec(initialCwd: string): Promise { +export async function loadSpec(initialCwd: string, skipSpecParsing?: SkipSpecParsing): Promise> { let nextCwd = initialCwd; let currCwd = ``; let selection: { data: any; manifestPath: string; + envFilePath?: string; + localEnv: LocalEnvFile; } | null = null; while (nextCwd !== currCwd && (!selection || !selection.data.packageManager)) { @@ -111,19 +210,60 @@ export async function loadSpec(initialCwd: string): Promise { if (typeof data !== `object` || data === null) throw new UsageError(`Invalid package.json in ${path.relative(initialCwd, manifestPath)}`); - selection = {data, manifestPath}; + let localEnv: LocalEnvFile; + const envFilePath = path.resolve(currCwd, process.env.COREPACK_ENV_FILE ?? `.corepack.env`); + if (process.env.COREPACK_ENV_FILE == `0`) { + debugUtils.log(`Skipping env file as configured with COREPACK_ENV_FILE`); + localEnv = process.env; + } else if (typeof parseEnv !== `function`) { + // TODO: remove this block when support for Node.js 18.x is dropped. + debugUtils.log(`Skipping env file as it is not supported by the current version of Node.js`); + localEnv = process.env; + } else { + debugUtils.log(`Checking ${envFilePath}`); + try { + localEnv = { + ...Object.fromEntries(Object.entries(parseEnv(await fs.promises.readFile(envFilePath, `utf8`))).filter(e => e[0].startsWith(`COREPACK_`))), + ...process.env, + }; + debugUtils.log(`Successfully loaded env file found at ${envFilePath}`); + } catch (err) { + if ((err as NodeError)?.code !== `ENOENT`) + throw err; + + debugUtils.log(`No env file found at ${envFilePath}`); + localEnv = process.env; + } + } + + selection = {data, manifestPath, localEnv, envFilePath}; } if (selection === null) return {type: `NoProject`, target: path.join(initialCwd, `package.json`)}; - const rawPmSpec = selection.data.packageManager; + let envFilePath: string | undefined; + if (selection.localEnv !== process.env) { + envFilePath = selection.envFilePath; + process.env = selection.localEnv; + } + + const rawPmSpec = parsePackageJSON(selection.data); if (typeof rawPmSpec === `undefined`) return {type: `NoSpec`, target: selection.manifestPath}; + debugUtils.log(`${selection.manifestPath} defines ${rawPmSpec} as local package manager`); + return { type: `Found`, target: selection.manifestPath, - spec: parseSpec(rawPmSpec, path.relative(initialCwd, selection.manifestPath)), + envFilePath, + range: selection.data.devEngines?.packageManager?.version && { + name: selection.data.devEngines.packageManager.name, + range: selection.data.devEngines.packageManager.version, + }, + spec: skipSpecParsing ? + (undefined as SkipSpecParsing extends true ? undefined : never) : + parseSpec(rawPmSpec, path.relative(initialCwd, selection.manifestPath)) as SkipSpecParsing extends true ? never : ReturnType, }; } diff --git a/sources/types.ts b/sources/types.ts index b9fe3e370..1af427ce1 100644 --- a/sources/types.ts +++ b/sources/types.ts @@ -160,3 +160,5 @@ export interface LazyLocator { */ reference: () => Promise; } + +export type LocalEnvFile = Record; diff --git a/tests/Up.test.ts b/tests/Up.test.ts index d0d102400..e954af590 100644 --- a/tests/Up.test.ts +++ b/tests/Up.test.ts @@ -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'; @@ -11,25 +12,122 @@ beforeEach(async () => { }); describe(`UpCommand`, () => { - it(`should upgrade the package manager from the current project`, async () => { - await xfs.mktempPromise(async cwd => { + describe(`should update the "packageManager" field from the current project`, () => { + it(`to the same major if no devEngines range`, async () => { + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), { + packageManager: `yarn@2.1.0`, + }); + + await expect(runCli(cwd, [`up`])).resolves.toMatchObject({ + exitCode: 0, + stderr: ``, + }); + + await expect(xfs.readJsonPromise(ppath.join(cwd, `package.json`))).resolves.toMatchObject({ + packageManager: `yarn@2.4.3+sha512.8dd9fedc5451829619e526c56f42609ad88ae4776d9d3f9456d578ac085115c0c2f0fb02bb7d57fd2e1b6e1ac96efba35e80a20a056668f61c96934f67694fd0`, + }); + + await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({ + exitCode: 0, + stdout: `2.4.3\n`, + }); + }); + }); + + it(`to whichever range devEngines defines`, async () => { + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), { + packageManager: `yarn@1.1.0`, + devEngines: { + packageManager: { + name: `yarn`, + version: `1.x || 2.x`, + }, + }, + }); + + await expect(runCli(cwd, [`up`])).resolves.toMatchObject({ + exitCode: 0, + stderr: ``, + }); + + await expect(xfs.readJsonPromise(ppath.join(cwd, `package.json`))).resolves.toMatchObject({ + packageManager: `yarn@2.4.3+sha512.8dd9fedc5451829619e526c56f42609ad88ae4776d9d3f9456d578ac085115c0c2f0fb02bb7d57fd2e1b6e1ac96efba35e80a20a056668f61c96934f67694fd0`, + }); + + await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({ + exitCode: 0, + stdout: `2.4.3\n`, + }); + }); + }); + + it(`to whichever range devEngines defines even if onFail is set to ignore`, async () => { + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), { + packageManager: `pnpm@10.1.0`, + devEngines: { + packageManager: { + name: `yarn`, + version: `1.x || 2.x`, + onFail: `ignore`, + }, + }, + }); + + await expect(runCli(cwd, [`up`])).resolves.toMatchObject({ + exitCode: 0, + stderr: ``, + }); + + await expect(xfs.readJsonPromise(ppath.join(cwd, `package.json`))).resolves.toMatchObject({ + packageManager: `yarn@2.4.3+sha512.8dd9fedc5451829619e526c56f42609ad88ae4776d9d3f9456d578ac085115c0c2f0fb02bb7d57fd2e1b6e1ac96efba35e80a20a056668f61c96934f67694fd0`, + }); + + await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({ + exitCode: 0, + stdout: `2.4.3\n`, + }); + }); + }); + }); + + 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`), { - packageManager: `yarn@2.1.0`, + 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➤ YN0000: (.*\n)+➤ YN0000: Done in \d+s \d+ms\n$/), }); - await expect(xfs.readJsonPromise(ppath.join(cwd, `package.json`))).resolves.toMatchObject({ - packageManager: `yarn@2.4.3+sha512.8dd9fedc5451829619e526c56f42609ad88ae4776d9d3f9456d578ac085115c0c2f0fb02bb7d57fd2e1b6e1ac96efba35e80a20a056668f61c96934f67694fd0`, - }); + 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: ``, }); - }); + }))); }); }); diff --git a/tests/Use.test.ts b/tests/Use.test.ts index e4d528efc..daf5826c1 100644 --- a/tests/Use.test.ts +++ b/tests/Use.test.ts @@ -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'; @@ -11,7 +12,7 @@ beforeEach(async () => { }); describe(`UseCommand`, () => { - it(`should set the package manager in the current project`, async () => { + it(`should update the "packageManager" field in the current project`, async () => { await xfs.mktempPromise(async cwd => { await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), { packageManager: `yarn@1.0.0`, @@ -32,6 +33,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({ @@ -64,4 +144,89 @@ describe(`UseCommand`, () => { }); }); }); + + describe(`should not care if packageManager is set to an invalid value`, () => { + for (const {description, packageManager} of [ + { + description: `when a version range is given`, + packageManager: `yarn@1.x`, + }, + { + description: `when only the pm name is given`, + packageManager: `yarn`, + }, + { + description: `when the version is missing`, + packageManager: `yarn@`, + }, + { + description: `when the field is not a string`, + packageManager: [], + }, + ]) { + it(description, async () => { + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), { + packageManager, + license: `MIT`, // To avoid warning + }); + + 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\nyarn install v1\.22\.4\ninfo No lockfile found\.\n(.*\n)+Done in \d+\.\d+s\.\n$/), + }); + + await expect(xfs.readJsonPromise(ppath.join(cwd, `package.json`))).resolves.toMatchObject({ + packageManager: `yarn@1.22.4+sha512.a1833b862fe52169bd6c2a033045a07df5bc6a23595c259e675fed1b2d035ab37abe6ce309720abb6636d68f03615054b6292dc0a70da31c8697fda228b50d18`, + }); + + await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({ + exitCode: 0, + stdout: `1.22.4\n`, + stderr: ``, + }); + }); + }); + } + }); + + 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\nyarn install v1\.22\.4\ninfo No lockfile found\.\n(.*\n)+Done in \d+\.\d+s\.\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: ``, + }); + }))); + }); }); diff --git a/tests/main.test.ts b/tests/main.test.ts index 91d10999c..b6189fb5c 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -16,16 +16,33 @@ beforeEach(async () => { process.env.COREPACK_DEFAULT_TO_LATEST = `0`; }); -it(`should refuse to download a package manager if the hash doesn't match`, async () => { - await xfs.mktempPromise(async cwd => { - await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), { - packageManager: `yarn@1.22.4+sha1.deadbeef`, +describe(`should refuse to download a package manager if the hash doesn't match`, () => { + it(`the one defined in "devEngines.packageManager" field`, async () => { + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), { + devEngines: { + packageManager: {name: `yarn`, version: `1.22.4+sha1.deadbeef`}, + }, + }); + + await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({ + exitCode: 1, + stderr: expect.stringContaining(`Mismatch hashes`), + stdout: ``, + }); }); + }); + it(`the one defined in "packageManager" field`, async () => { + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), { + packageManager: `yarn@1.22.4+sha1.deadbeef`, + }); - await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({ - exitCode: 1, - stderr: expect.stringContaining(`Mismatch hashes`), - stdout: ``, + await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({ + exitCode: 1, + stderr: expect.stringContaining(`Mismatch hashes`), + stdout: ``, + }); }); }); }); @@ -150,6 +167,20 @@ for (const [name, version, expectedVersion = version.split(`+`, 1)[0]] of tested stderr: ``, stdout: `${expectedVersion}\n`, }); + + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), { + devEngines: {packageManager: {name, version}}, + }); + + await expect(runCli(cwd, [name, `--version`])).resolves.toMatchObject(URL.canParse(version) ? { + exitCode: 1, + stderr: expect.stringMatching(/^The value of devEngines\.packageManager\.version ".+" is not a valid semver range\n$/), + stdout: ``, + } : { + exitCode: 0, + stderr: ``, + stdout: `${expectedVersion}\n`, + }); }); }); } @@ -231,6 +262,298 @@ it(`should ignore the packageManager field when found within a node_modules vend }); }); +describe(`should handle invalid devEngines values`, () => { + it(`throw on missing version`, async () => { + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as PortablePath), { + devEngines: { + packageManager: { + name: `yarn`, + }, + }, + }); + + await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({ + exitCode: 1, + stderr: `Invalid package manager specification in package.json (yarn@*); expected a semver version\n`, + stdout: ``, + }); + }); + }); + it(`throw on invalid version`, async () => { + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as PortablePath), { + devEngines: { + packageManager: { + name: `yarn`, + version: `yarn@1.x`, + }, + }, + }); + + await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({ + exitCode: 1, + stderr: `The value of devEngines.packageManager.version "yarn@1.x" is not a valid semver range\n`, + stdout: ``, + }); + }); + }); + it(`warn on array values`, async () => { + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as PortablePath), { + packageManager: `yarn@1.22.4+sha1.01c1197ca5b27f21edc8bc472cd4c8ce0e5a470e`, + devEngines: { + packageManager: [{ + name: `pnpm`, + version: `10.x`, + }, + ]}, + }); + + await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({ + exitCode: 0, + stderr: `! Corepack does not currently support array values for devEngines.packageManager\n`, + stdout: `1.22.4\n`, + }); + }); + }); + it(`warn on string values`, async () => { + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as PortablePath), { + packageManager: `yarn@1.22.4+sha1.01c1197ca5b27f21edc8bc472cd4c8ce0e5a470e`, + devEngines: { + packageManager: `pnpm@10.x`, + }, + }); + + await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({ + exitCode: 0, + stderr: `! Corepack only supports objects as valid value for devEngines.packageManager. The current value ("pnpm@10.x") will be ignored.\n`, + stdout: `1.22.4\n`, + }); + }); + }); + it(`warn on number values`, async () => { + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as PortablePath), { + packageManager: `yarn@1.22.4+sha1.01c1197ca5b27f21edc8bc472cd4c8ce0e5a470e`, + devEngines: { + packageManager: 10, + }, + }); + + await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({ + exitCode: 0, + stderr: `! Corepack only supports objects as valid value for devEngines.packageManager. The current value (10) will be ignored.\n`, + stdout: `1.22.4\n`, + }); + }); + }); +}); + +it(`should use hash from "packageManager" even when "devEngines" defines a different one`, async () => { + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as PortablePath), { + packageManager: `yarn@3.0.0-rc.2+sha1.11111`, + devEngines: { + packageManager: { + name: `yarn`, + version: `3.0.0-rc.2+sha1.22222`, + }, + }, + }); + + await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({ + exitCode: 1, + stderr: expect.stringContaining(`Mismatch hashes. Expected 11111, got`), + stdout: ``, + }); + }); +}); + +describe(`should accept range in devEngines only if a specific version is provided`, () => { + it(`either in package.json#packageManager field`, async () => { + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as PortablePath), { + devEngines: { + packageManager: { + name: `pnpm`, + version: `6.x`, + }, + }, + }); + await expect(runCli(cwd, [`pnpm`, `--version`])).resolves.toMatchObject({ + exitCode: 1, + stderr: `Invalid package manager specification in package.json (pnpm@6.x); expected a semver version\n`, + stdout: ``, + }); + + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as PortablePath), { + devEngines: { + packageManager: { + name: `pnpm`, + version: `6.x`, + }, + }, + packageManager: `pnpm@6.6.2+sha224.eb5c0acad3b0f40ecdaa2db9aa5a73134ad256e17e22d1419a2ab073`, + }); + await expect(runCli(cwd, [`pnpm`, `--version`])).resolves.toMatchObject({ + exitCode: 0, + stderr: ``, + stdout: `6.6.2\n`, + }); + + // No version should also work + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as PortablePath), { + devEngines: { + packageManager: { + name: `pnpm`, + }, + }, + packageManager: `pnpm@6.6.2+sha224.eb5c0acad3b0f40ecdaa2db9aa5a73134ad256e17e22d1419a2ab073`, + }); + await expect(runCli(cwd, [`pnpm`, `--version`])).resolves.toMatchObject({ + exitCode: 0, + stderr: ``, + stdout: `6.6.2\n`, + }); + }); + }); +}); + +describe(`when devEngines.packageManager.name does not match packageManager`, () => { + it(`should ignore if devEngines.packageManager.onFail is set to "ignore"`, async () => { + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as PortablePath), { + devEngines: { + packageManager: { + name: `yarn`, + onFail: `ignore`, + }, + }, + packageManager: `pnpm@6.6.2+sha1.7b4d6b176c1b93b5670ed94c24babb7d80c13854`, + }); + await expect(runCli(cwd, [`pnpm`, `--version`])).resolves.toMatchObject({ + exitCode: 0, + stderr: ``, + stdout: `6.6.2\n`, + }); + }); + }); + it(`should warn if devEngines.packageManager.onFail is set to "warn"`, async () => { + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as PortablePath), { + devEngines: { + packageManager: { + name: `yarn`, + onFail: `warn`, + }, + }, + packageManager: `pnpm@6.6.2+sha1.7b4d6b176c1b93b5670ed94c24babb7d80c13854`, + }); + await expect(runCli(cwd, [`pnpm`, `--version`])).resolves.toMatchObject({ + exitCode: 0, + stderr: `! Corepack validation warning: "packageManager" field is set to "pnpm@6.6.2+sha1.7b4d6b176c1b93b5670ed94c24babb7d80c13854" which does not match the "devEngines.packageManager" field set to "yarn"\n`, + stdout: `6.6.2\n`, + }); + }); + }); + it(`should throw if devEngines.packageManager.onFail is set to "error"`, async () => { + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as PortablePath), { + devEngines: { + packageManager: { + name: `yarn`, + onFail: `error`, + }, + }, + packageManager: `pnpm@6.6.2+sha1.7b4d6b176c1b93b5670ed94c24babb7d80c13854`, + }); + await expect(runCli(cwd, [`pnpm`, `--version`])).resolves.toMatchObject({ + exitCode: 1, + stderr: `"packageManager" field is set to "pnpm@6.6.2+sha1.7b4d6b176c1b93b5670ed94c24babb7d80c13854" which does not match the "devEngines.packageManager" field set to "yarn"\n`, + stdout: ``, + }); + }); + }); + it(`should throw by default`, async () => { + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as PortablePath), { + devEngines: { + packageManager: { + name: `yarn`, + }, + }, + packageManager: `pnpm@6.6.2+sha1.7b4d6b176c1b93b5670ed94c24babb7d80c13854`, + }); + await expect(runCli(cwd, [`pnpm`, `--version`])).resolves.toMatchObject({ + exitCode: 1, + stderr: `"packageManager" field is set to "pnpm@6.6.2+sha1.7b4d6b176c1b93b5670ed94c24babb7d80c13854" which does not match the "devEngines.packageManager" field set to "yarn"\n`, + stdout: ``, + }); + }); + }); +}); + +describe(`should reject if range in devEngines does not match version provided`, () => { + it(`unless onFail is set to "ignore"`, async () => { + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as PortablePath), { + devEngines: { + packageManager: { + name: `pnpm`, + version: `10.x`, + onFail: `ignore`, + }, + }, + packageManager: `pnpm@6.6.2+sha1.7b4d6b176c1b93b5670ed94c24babb7d80c13854`, + }); + await expect(runCli(cwd, [`pnpm`, `--version`])).resolves.toMatchObject({ + exitCode: 0, + stderr: ``, + stdout: `6.6.2\n`, + }); + }); + }); + it(`unless onFail is set to "warn"`, async () => { + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as PortablePath), { + devEngines: { + packageManager: { + name: `pnpm`, + version: `10.x`, + onFail: `warn`, + }, + }, + packageManager: `pnpm@6.6.2+sha1.7b4d6b176c1b93b5670ed94c24babb7d80c13854`, + }); + await expect(runCli(cwd, [`pnpm`, `--version`])).resolves.toMatchObject({ + exitCode: 0, + stderr: `! Corepack validation warning: "packageManager" field is set to "pnpm@6.6.2+sha1.7b4d6b176c1b93b5670ed94c24babb7d80c13854" which does not match the value defined in "devEngines.packageManager" for "pnpm" of "10.x"\n`, + stdout: `6.6.2\n`, + }); + }); + }); + it(`in package.json#packageManager field`, async () => { + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as PortablePath), { + devEngines: { + packageManager: { + name: `pnpm`, + version: `10.x`, + }, + }, + packageManager: `pnpm@6.6.2+sha1.7b4d6b176c1b93b5670ed94c24babb7d80c13854`, + }); + await expect(runCli(cwd, [`pnpm`, `--version`])).resolves.toMatchObject({ + exitCode: 1, + stderr: `"packageManager" field is set to "pnpm@6.6.2+sha1.7b4d6b176c1b93b5670ed94c24babb7d80c13854" which does not match the value defined in "devEngines.packageManager" for "pnpm" of "10.x"\n`, + stdout: ``, + }); + }); + }); +}); + it(`should use the closest matching packageManager field`, async () => { await xfs.mktempPromise(async cwd => { await xfs.mkdirPromise(ppath.join(cwd, `foo` as PortablePath), {recursive: true}); @@ -318,18 +641,72 @@ for (const name of SupportedPackageManagerSet) { }); } -it(`should configure the project when calling a package manager on it for the first time`, async () => { - await xfs.mktempPromise(async cwd => { - await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), { +describe(`when called on a project without any defined packageManager`, () => { + it(`should append the field to package.json by default`, async () => { + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), { + // empty package.json file + }); + + await runCli(cwd, [`yarn`]); + + const data = await xfs.readJsonPromise(ppath.join(cwd, `package.json` as Filename)); + + expect(data).toMatchObject({ + packageManager: `yarn@${config.definitions.yarn.default}`, + }); + }); + }); + + it(`should not modify package.json if disabled by env`, async () => { + process.env.COREPACK_ENABLE_AUTO_PIN = `0`; + + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), { // empty package.json file + }); + + await runCli(cwd, [`yarn`]); + + const data = await xfs.readJsonPromise(ppath.join(cwd, `package.json` as Filename)); + + expect(Object.hasOwn(data, `packageManager`)).toBeFalsy(); + }); + }); + + it(`should not modify package.json if disabled by .corepack.env`, 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 xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), { + // empty package.json file + }); + await xfs.writeFilePromise(ppath.join(cwd, `.corepack.env` as Filename), `COREPACK_ENABLE_AUTO_PIN=0\n`); + + await runCli(cwd, [`yarn`]); + + const data = await xfs.readJsonPromise(ppath.join(cwd, `package.json` as Filename)); + + expect(Object.hasOwn(data, `packageManager`)).toBeFalsy(); }); + }); + it(`should modify package.json if .corepack.env if disabled`, async () => { + process.env.COREPACK_ENV_FILE = `0`; + + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), { + // empty package.json file + }); + await xfs.writeFilePromise(ppath.join(cwd, `.corepack.env` as Filename), `COREPACK_ENABLE_AUTO_PIN=0\n`); - await runCli(cwd, [`yarn`]); + await runCli(cwd, [`yarn`]); - const data = await xfs.readJsonPromise(ppath.join(cwd, `package.json` as Filename)); + const data = await xfs.readJsonPromise(ppath.join(cwd, `package.json` as Filename)); - expect(data).toMatchObject({ - packageManager: `yarn@${config.definitions.yarn.default}`, + expect(data).toMatchObject({ + packageManager: `yarn@${config.definitions.yarn.default}`, + }); }); }); }); @@ -806,16 +1183,35 @@ it(`should support package managers in ESM format`, async () => { }); }); -it(`should show a warning on stderr before downloading when enable`, async() => { - await xfs.mktempPromise(async cwd => { - process.env.COREPACK_ENABLE_DOWNLOAD_PROMPT = `1`; - await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), { - packageManager: `yarn@3.0.0`, +describe(`should show a warning on stderr before downloading when enable`, () => { + it(`when enabled by the environment`, async () => { + await xfs.mktempPromise(async cwd => { + process.env.COREPACK_ENABLE_DOWNLOAD_PROMPT = `1`; + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), { + packageManager: `yarn@3.0.0`, + }); + await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({ + exitCode: 0, + stdout: `3.0.0\n`, + stderr: `! Corepack is about to download https://repo.yarnpkg.com/3.0.0/packages/yarnpkg-cli/bin/yarn.js\n`, + }); }); - await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({ - exitCode: 0, - stdout: `3.0.0\n`, - stderr: `! Corepack is about to download https://repo.yarnpkg.com/3.0.0/packages/yarnpkg-cli/bin/yarn.js\n`, + }); + + it(`should ignore setting in .corepack.env`, async () => { + await xfs.mktempPromise(async cwd => { + await xfs.writeFilePromise( + ppath.join(cwd, `.corepack.env` as Filename), + `COREPACK_ENABLE_DOWNLOAD_PROMPT=1\n`, + ); + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), { + packageManager: `yarn@3.0.0`, + }); + await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({ + exitCode: 0, + stdout: `3.0.0\n`, + stderr: ``, + }); }); }); }); @@ -906,6 +1302,156 @@ it(`should download latest pnpm from custom registry`, async () => { }); }); +describe(`should pick up COREPACK_INTEGRITY_KEYS from env`, () => { + beforeEach(() => { + process.env.AUTH_TYPE = `COREPACK_NPM_TOKEN`; // See `_registryServer.mjs` + process.env.COREPACK_DEFAULT_TO_LATEST = `1`; + }); + + it(`from env variable`, async () => { + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), { + }); + + await expect(runCli(cwd, [`pnpm`, `--version`], true)).resolves.toMatchObject({ + exitCode: 1, + stdout: ``, + stderr: expect.stringContaining(`No compatible signature found in package metadata`), + }); + + process.env.COREPACK_INTEGRITY_KEYS = `0`; + await expect(runCli(cwd, [`pnpm`, `--version`], true)).resolves.toMatchObject({ + exitCode: 0, + stdout: `pnpm: Hello from custom registry\n`, + stderr: expect.stringContaining(`The local project doesn't define a 'packageManager' field`), + }); + }); + }); + + it(`from .corepack.env file`, 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 xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), { + }); + + await expect(runCli(cwd, [`pnpm`, `--version`], true)).resolves.toMatchObject({ + exitCode: 1, + stdout: ``, + stderr: expect.stringContaining(`No compatible signature found in package metadata`), + }); + + await xfs.writeFilePromise(ppath.join(cwd, `.corepack.env` as Filename), `COREPACK_INTEGRITY_KEYS=0\n`); + await expect(runCli(cwd, [`pnpm`, `--version`], true)).resolves.toMatchObject({ + exitCode: 0, + stdout: `pnpm: Hello from custom registry\n`, + stderr: expect.stringContaining(`The local project doesn't define a 'packageManager' field`), + }); + }); + }); + + it(`from env file defined by COREPACK_ENV_FILE`, 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 xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), { + }); + + await xfs.writeFilePromise(ppath.join(cwd, `.corepack.env` as Filename), `COREPACK_INTEGRITY_KEYS={}\n`); + await xfs.writeFilePromise(ppath.join(cwd, `.other.env` as Filename), `COREPACK_INTEGRITY_KEYS=0\n`); + + // By default, Corepack should be using .corepack.env and fail. + await expect(runCli(cwd, [`pnpm`, `--version`], true)).resolves.toMatchObject({ + exitCode: 1, + stdout: ``, + stderr: expect.stringContaining(`No compatible signature found in package metadata`), + }); + + process.env.COREPACK_ENV_FILE = `.other.env`; + await expect(runCli(cwd, [`pnpm`, `--version`], true)).resolves.toMatchObject({ + exitCode: 0, + stdout: `pnpm: Hello from custom registry\n`, + stderr: expect.stringContaining(`The local project doesn't define a 'packageManager' field`), + }); + }); + }); + + it(`from env even if there's a .corepack.env file`, async () => { + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), { + }); + + await xfs.writeFilePromise(ppath.join(cwd, `.corepack.env` as Filename), `COREPACK_INTEGRITY_KEYS={}\n`); + + // By default, Corepack should be using .corepack.env (or the built-in ones on Node.js 18.x) and fail. + await expect(runCli(cwd, [`pnpm`, `--version`], true)).resolves.toMatchObject({ + exitCode: 1, + stdout: ``, + stderr: expect.stringContaining(`No compatible signature found in package metadata`), + }); + + process.env.COREPACK_INTEGRITY_KEYS = ``; + await expect(runCli(cwd, [`pnpm`, `--version`], true)).resolves.toMatchObject({ + exitCode: 0, + stdout: `pnpm: Hello from custom registry\n`, + stderr: expect.stringContaining(`The local project doesn't define a 'packageManager' field`), + }); + }); + }); + + it(`should ignore .corepack.env file if COREPACK_ENV_FILE is set to 0`, 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 xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), { + }); + + await xfs.writeFilePromise(ppath.join(cwd, `.corepack.env` as Filename), `COREPACK_INTEGRITY_KEYS=0\n`); + + process.env.COREPACK_ENV_FILE = `0`; + await expect(runCli(cwd, [`pnpm`, `--version`], true)).resolves.toMatchObject({ + exitCode: 1, + stdout: ``, + stderr: expect.stringContaining(`No compatible signature found in package metadata`), + }); + + delete process.env.COREPACK_ENV_FILE; + await expect(runCli(cwd, [`pnpm`, `--version`], true)).resolves.toMatchObject({ + exitCode: 0, + stdout: `pnpm: Hello from custom registry\n`, + stderr: expect.stringContaining(`The local project doesn't define a 'packageManager' field`), + }); + }); + }); + + it(`from env file defined by COREPACK_ENV_FILE`, async t => { + // Skip that test on Node.js 18.x as it lacks support for .env files. + if (process.version.startsWith(`v18.`)) t.skip(); + + process.env.COREPACK_ENV_FILE = `.other.env`; + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), { + }); + + await expect(runCli(cwd, [`pnpm`, `--version`], true)).resolves.toMatchObject({ + exitCode: 1, + stdout: ``, + stderr: expect.stringContaining(`No compatible signature found in package metadata`), + }); + + await xfs.writeFilePromise(ppath.join(cwd, `.other.env` as Filename), `COREPACK_INTEGRITY_KEYS=0\n`); + await expect(runCli(cwd, [`pnpm`, `--version`], true)).resolves.toMatchObject({ + exitCode: 0, + stdout: `pnpm: Hello from custom registry\n`, + stderr: expect.stringContaining(`The local project doesn't define a 'packageManager' field`), + }); + }); + }); +}); + for (const authType of [`COREPACK_NPM_REGISTRY`, `COREPACK_NPM_TOKEN`, `COREPACK_NPM_PASSWORD`, `PROXY`]) { describe(`custom registry with auth ${authType}`, () => { beforeEach(() => {