diff --git a/sources/Engine.ts b/sources/Engine.ts index f445d7d58..c818cb2b9 100644 --- a/sources/Engine.ts +++ b/sources/Engine.ts @@ -264,7 +264,7 @@ export class Engine { transparent = true; while (true) { - const result = await specUtils.loadSpec(initialCwd); + const result = await specUtils.loadSpecAndEnv(initialCwd); switch (result.type) { case `NoProject`: { diff --git a/sources/commands/Base.ts b/sources/commands/Base.ts index 79e3402a5..c2c9ea2de 100644 --- a/sources/commands/Base.ts +++ b/sources/commands/Base.ts @@ -10,7 +10,7 @@ export abstract class BaseCommand extends Command { const resolvedSpecs = patterns.map(pattern => specUtils.parseSpec(pattern, `CLI arguments`, {enforceExactVersion: false})); if (resolvedSpecs.length === 0) { - const lookup = await specUtils.loadSpec(this.context.cwd); + const lookup = await specUtils.loadSpecAndEnv(this.context.cwd); switch (lookup.type) { case `NoProject`: throw new UsageError(`Couldn't find a project in the local directory - please specify the package manager to pack, or run this command from a valid project`); @@ -22,6 +22,8 @@ export abstract class BaseCommand extends Command { return [lookup.range ?? lookup.getSpec()]; } } + } else { + await specUtils.loadSpecAndEnv(this.context.cwd, {envOnly: true}); } return resolvedSpecs; diff --git a/sources/commands/deprecated/Prepare.ts b/sources/commands/deprecated/Prepare.ts index 0f33ecd19..49705b900 100644 --- a/sources/commands/deprecated/Prepare.ts +++ b/sources/commands/deprecated/Prepare.ts @@ -33,7 +33,7 @@ export class PrepareCommand extends Command { const installLocations: Array = []; if (specs.length === 0) { - const lookup = await specUtils.loadSpec(this.context.cwd); + const lookup = await specUtils.loadSpecAndEnv(this.context.cwd); switch (lookup.type) { case `NoProject`: throw new UsageError(`Couldn't find a project in the local directory - please specify the package manager to pack, or run this command from a valid project`); @@ -45,6 +45,8 @@ export class PrepareCommand extends Command { specs.push(lookup.getSpec()); } } + } else { + await specUtils.loadSpecAndEnv(this.context.cwd, {envOnly: true}); } for (const request of specs) { diff --git a/sources/specUtils.ts b/sources/specUtils.ts index a1915ac0b..29f61f59a 100644 --- a/sources/specUtils.ts +++ b/sources/specUtils.ts @@ -121,7 +121,7 @@ function parsePackageJSON(packageJSONContent: CorepackPackageJSON) { } export async function setLocalPackageManager(cwd: string, info: PreparedPackageManagerInfo) { - const lookup = await loadSpec(cwd); + const lookup = await loadSpecAndEnv(cwd); const range = `range` in lookup && lookup.range; if (range) { @@ -155,20 +155,42 @@ interface FoundSpecResult { envFilePath?: string; } export type LoadSpecResult = - | {type: `NoProject`, target: string} - | {type: `NoSpec`, target: string} + | {type: `NoProject`, target: string, envFilePath?: string} + | {type: `NoSpec`, target: string, envFilePath?: string} | FoundSpecResult; -export async function loadSpec(initialCwd: string): Promise { +async function loadEnvFileIfExists(cwd: string): Promise<{env: LocalEnvFile, path: string} | void> { + const envFilePath = path.resolve(cwd, 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`); + return void 0; + } + debugUtils.log(`Checking ${envFilePath}`); + try { + const 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}`); + return {env: localEnv, path: envFilePath}; + } catch (err) { + if ((err as NodeError)?.code !== `ENOENT`) + throw err; + + debugUtils.log(`No env file found at ${envFilePath}`); + } + return void 0; +} + +export async function loadSpecAndEnv(initialCwd: string, {envOnly} = {envOnly: false}): Promise { let nextCwd = initialCwd; let currCwd = ``; let selection: { data: any; manifestPath: string; - envFilePath?: string; - localEnv: LocalEnvFile; } | null = null; + let localEnv: {env: LocalEnvFile, path: string} | void = void 0; while (nextCwd !== currCwd && (!selection || !selection.data.packageManager)) { currCwd = nextCwd; @@ -177,6 +199,14 @@ export async function loadSpec(initialCwd: string): Promise { if (nodeModulesRegExp.test(currCwd)) continue; + if (process.env.COREPACK_ENV_FILE !== `0` && !localEnv) + localEnv = await loadEnvFileIfExists(currCwd); + + if (envOnly) { + if (localEnv) break; + continue; + } + const manifestPath = path.join(currCwd, `package.json`); debugUtils.log(`Checking ${manifestPath}`); let content: string; @@ -193,56 +223,27 @@ export async function loadSpec(initialCwd: string): Promise { } catch {} if (typeof data !== `object` || data === null) - throw new UsageError(`Invalid package.json in ${path.relative(initialCwd, 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; - } - } + throw new UsageError(`Invalid package.json in ${path.relative(currCwd, manifestPath)}`); - selection = {data, manifestPath, localEnv, envFilePath}; + selection = {data, manifestPath}; } - if (selection === null) - return {type: `NoProject`, target: path.join(initialCwd, `package.json`)}; + if (localEnv) + process.env = localEnv.env; - let envFilePath: string | undefined; - if (selection.localEnv !== process.env) { - envFilePath = selection.envFilePath; - process.env = selection.localEnv; - } + if (selection === null) + return {type: `NoProject`, target: path.join(initialCwd, `package.json`), envFilePath: localEnv?.path}; const rawPmSpec = parsePackageJSON(selection.data); if (typeof rawPmSpec === `undefined`) - return {type: `NoSpec`, target: selection.manifestPath}; + return {type: `NoSpec`, target: selection.manifestPath, envFilePath: localEnv?.path}; debugUtils.log(`${selection.manifestPath} defines ${rawPmSpec} as local package manager`); return { type: `Found`, target: selection.manifestPath, - envFilePath, + envFilePath: localEnv?.path, range: selection.data.devEngines?.packageManager?.version && { name: selection.data.devEngines.packageManager.name, range: selection.data.devEngines.packageManager.version, diff --git a/tests/main.test.ts b/tests/main.test.ts index fac93914d..e6f7d7200 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -1322,6 +1322,95 @@ it(`should download latest pnpm from custom registry`, async () => { }); }); +it(`should use COREPACK_NPM_REGISTRY from .corepack.env for "corepack use" command`, async () => { + process.env.COREPACK_ENABLE_NETWORK = `0`; + + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {}); + + // Set COREPACK_NPM_REGISTRY in .corepack.env + await xfs.writeFilePromise(ppath.join(cwd, `.corepack.env` as Filename), `COREPACK_NPM_REGISTRY=http://custom-registry.example.com\n`); + + // "corepack use pnpm" should read .corepack.env and use the custom registry + // When network is disabled, the error message should contain the custom registry URL + await expect(runCli(cwd, [`use`, `pnpm`])).resolves.toMatchObject({ + stderr: ``, + stdout: expect.stringContaining(`custom-registry.example.com`), + exitCode: 1, + }); + }); +}); + +it(`should use closest .corepack.env`, async () => { + process.env.COREPACK_ENABLE_NETWORK = `0`; + process.env.DEBUG = `corepack`; + + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), { + packageManager: `yarn@1.22.4+sha1.01c1197ca5b27f21edc8bc472cd4c8ce0e5a470e`, + }); + + // Set COREPACK_NPM_REGISTRY in .corepack.env + await xfs.writeFilePromise(ppath.join(cwd, `.corepack.env` as Filename), `COREPACK_NPM_REGISTRY=http://root.example.com\n`); + await xfs.mkdirPromise(ppath.join(cwd, `subdir`)); + await xfs.writeFilePromise(ppath.join(cwd, `subdir`, `.corepack.env` as Filename), `COREPACK_NPM_REGISTRY=http://subdir.example.com\n`); + + // "corepack yarn --version" should read .corepack.env and use the custom registry + // When network is disabled, the error message should contain the custom registry URL + await expect(runCli(ppath.join(cwd, `subdir`), [`yarn`, `--version`])).resolves.toMatchObject({ + stdout: ``, + stderr: expect.stringContaining(`subdir.example.com`), + exitCode: 1, + }); + }); +}); + +it(`should ignore .corepack.env outside of the root`, async () => { + process.env.COREPACK_ENABLE_NETWORK = `0`; + process.env.DEBUG = `corepack`; + + await xfs.mktempPromise(async cwd => { + // Set COREPACK_NPM_REGISTRY in a .corepack.env outside of the repo root + await xfs.writeFilePromise(ppath.join(cwd, `.corepack.env` as Filename), `COREPACK_NPM_REGISTRY=http://above-root.example.com\n`); + await xfs.mkdirPromise(ppath.join(cwd, `repo-root`)); + await xfs.writeJsonPromise(ppath.join(cwd, `repo-root`, `package.json` as Filename), { + packageManager: `yarn@1.22.4+sha1.01c1197ca5b27f21edc8bc472cd4c8ce0e5a470e`, + }); + + // "corepack yarn --version" should NOT read .corepack.env and NOT use the custom registry + // When network is disabled, the error message should contain the custom registry URL + await expect(runCli(ppath.join(cwd, `repo-root`), [`yarn`, `--version`])).resolves.toMatchObject({ + stdout: ``, + stderr: expect.not.stringContaining(`above-root.example.com`), + exitCode: 1, + }); + }); +}); + +it(`should ignore .corepack.env inside a node_modules folder`, async () => { + process.env.COREPACK_ENABLE_NETWORK = `0`; + process.env.DEBUG = `corepack`; + + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), { + packageManager: `yarn@1.22.4+sha1.01c1197ca5b27f21edc8bc472cd4c8ce0e5a470e`, + }); + + // Set COREPACK_NPM_REGISTRY in a .corepack.env from a node_modules package + await xfs.mkdirPromise(ppath.join(cwd, `node_modules`)); + await xfs.mkdirPromise(ppath.join(cwd, `node_modules`, `pkg`)); + await xfs.writeFilePromise(ppath.join(cwd, `node_modules`, `pkg`, `.corepack.env` as Filename), `COREPACK_NPM_REGISTRY=http://npm-pkg.example.com\n`); + + // "corepack yarn --version" should NOT read .corepack.env and NOT use the custom registry + // When network is disabled, the error message should contain the custom registry URL + await expect(runCli(ppath.join(cwd, `node_modules`, `pkg`), [`yarn`, `--version`])).resolves.toMatchObject({ + stdout: ``, + stderr: expect.not.stringContaining(`npm-pkg.example.com`), + exitCode: 1, + }); + }); +}); + describe(`should pick up COREPACK_INTEGRITY_KEYS from env`, () => { beforeEach(() => { process.env.AUTH_TYPE = `COREPACK_NPM_TOKEN`; // See `_registryServer.mjs`