Skip to content

Commit b856c51

Browse files
authored
feat: only load closest env file, for every commands (#891)
Previously it would be skipped for e.g. `corepack use`.
1 parent 8ca01c3 commit b856c51

5 files changed

Lines changed: 140 additions & 46 deletions

File tree

‎sources/Engine.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ export class Engine {
264264
transparent = true;
265265

266266
while (true) {
267-
const result = await specUtils.loadSpec(initialCwd);
267+
const result = await specUtils.loadSpecAndEnv(initialCwd);
268268

269269
switch (result.type) {
270270
case `NoProject`: {

‎sources/commands/Base.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export abstract class BaseCommand extends Command<Context> {
1010
const resolvedSpecs = patterns.map(pattern => specUtils.parseSpec(pattern, `CLI arguments`, {enforceExactVersion: false}));
1111

1212
if (resolvedSpecs.length === 0) {
13-
const lookup = await specUtils.loadSpec(this.context.cwd);
13+
const lookup = await specUtils.loadSpecAndEnv(this.context.cwd);
1414
switch (lookup.type) {
1515
case `NoProject`:
1616
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<Context> {
2222
return [lookup.range ?? lookup.getSpec()];
2323
}
2424
}
25+
} else {
26+
await specUtils.loadSpecAndEnv(this.context.cwd, {envOnly: true});
2527
}
2628

2729
return resolvedSpecs;

‎sources/commands/deprecated/Prepare.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export class PrepareCommand extends Command<Context> {
3333
const installLocations: Array<string> = [];
3434

3535
if (specs.length === 0) {
36-
const lookup = await specUtils.loadSpec(this.context.cwd);
36+
const lookup = await specUtils.loadSpecAndEnv(this.context.cwd);
3737
switch (lookup.type) {
3838
case `NoProject`:
3939
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<Context> {
4545
specs.push(lookup.getSpec());
4646
}
4747
}
48+
} else {
49+
await specUtils.loadSpecAndEnv(this.context.cwd, {envOnly: true});
4850
}
4951

5052
for (const request of specs) {

‎sources/specUtils.ts‎

Lines changed: 44 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ function parsePackageJSON(packageJSONContent: CorepackPackageJSON) {
121121
}
122122

123123
export async function setLocalPackageManager(cwd: string, info: PreparedPackageManagerInfo) {
124-
const lookup = await loadSpec(cwd);
124+
const lookup = await loadSpecAndEnv(cwd);
125125

126126
const range = `range` in lookup && lookup.range;
127127
if (range) {
@@ -155,20 +155,42 @@ interface FoundSpecResult {
155155
envFilePath?: string;
156156
}
157157
export type LoadSpecResult =
158-
| {type: `NoProject`, target: string}
159-
| {type: `NoSpec`, target: string}
158+
| {type: `NoProject`, target: string, envFilePath?: string}
159+
| {type: `NoSpec`, target: string, envFilePath?: string}
160160
| FoundSpecResult;
161161

162-
export async function loadSpec(initialCwd: string): Promise<LoadSpecResult> {
162+
async function loadEnvFileIfExists(cwd: string): Promise<{env: LocalEnvFile, path: string} | void> {
163+
const envFilePath = path.resolve(cwd, process.env.COREPACK_ENV_FILE ?? `.corepack.env`);
164+
if (process.env.COREPACK_ENV_FILE == `0`) {
165+
debugUtils.log(`Skipping env file as configured with COREPACK_ENV_FILE`);
166+
return void 0;
167+
}
168+
debugUtils.log(`Checking ${envFilePath}`);
169+
try {
170+
const localEnv = {
171+
...Object.fromEntries(Object.entries(parseEnv(await fs.promises.readFile(envFilePath, `utf8`))).filter(e => e[0].startsWith(`COREPACK_`))),
172+
...process.env,
173+
};
174+
debugUtils.log(`Successfully loaded env file found at ${envFilePath}`);
175+
return {env: localEnv, path: envFilePath};
176+
} catch (err) {
177+
if ((err as NodeError)?.code !== `ENOENT`)
178+
throw err;
179+
180+
debugUtils.log(`No env file found at ${envFilePath}`);
181+
}
182+
return void 0;
183+
}
184+
185+
export async function loadSpecAndEnv(initialCwd: string, {envOnly} = {envOnly: false}): Promise<LoadSpecResult> {
163186
let nextCwd = initialCwd;
164187
let currCwd = ``;
165188

166189
let selection: {
167190
data: any;
168191
manifestPath: string;
169-
envFilePath?: string;
170-
localEnv: LocalEnvFile;
171192
} | null = null;
193+
let localEnv: {env: LocalEnvFile, path: string} | void = void 0;
172194

173195
while (nextCwd !== currCwd && (!selection || !selection.data.packageManager)) {
174196
currCwd = nextCwd;
@@ -177,6 +199,14 @@ export async function loadSpec(initialCwd: string): Promise<LoadSpecResult> {
177199
if (nodeModulesRegExp.test(currCwd))
178200
continue;
179201

202+
if (process.env.COREPACK_ENV_FILE !== `0` && !localEnv)
203+
localEnv = await loadEnvFileIfExists(currCwd);
204+
205+
if (envOnly) {
206+
if (localEnv) break;
207+
continue;
208+
}
209+
180210
const manifestPath = path.join(currCwd, `package.json`);
181211
debugUtils.log(`Checking ${manifestPath}`);
182212
let content: string;
@@ -193,56 +223,27 @@ export async function loadSpec(initialCwd: string): Promise<LoadSpecResult> {
193223
} catch {}
194224

195225
if (typeof data !== `object` || data === null)
196-
throw new UsageError(`Invalid package.json in ${path.relative(initialCwd, manifestPath)}`);
197-
198-
let localEnv: LocalEnvFile;
199-
const envFilePath = path.resolve(currCwd, process.env.COREPACK_ENV_FILE ?? `.corepack.env`);
200-
if (process.env.COREPACK_ENV_FILE == `0`) {
201-
debugUtils.log(`Skipping env file as configured with COREPACK_ENV_FILE`);
202-
localEnv = process.env;
203-
} else if (typeof parseEnv !== `function`) {
204-
// TODO: remove this block when support for Node.js 18.x is dropped.
205-
debugUtils.log(`Skipping env file as it is not supported by the current version of Node.js`);
206-
localEnv = process.env;
207-
} else {
208-
debugUtils.log(`Checking ${envFilePath}`);
209-
try {
210-
localEnv = {
211-
...Object.fromEntries(Object.entries(parseEnv(await fs.promises.readFile(envFilePath, `utf8`))).filter(e => e[0].startsWith(`COREPACK_`))),
212-
...process.env,
213-
};
214-
debugUtils.log(`Successfully loaded env file found at ${envFilePath}`);
215-
} catch (err) {
216-
if ((err as NodeError)?.code !== `ENOENT`)
217-
throw err;
218-
219-
debugUtils.log(`No env file found at ${envFilePath}`);
220-
localEnv = process.env;
221-
}
222-
}
226+
throw new UsageError(`Invalid package.json in ${path.relative(currCwd, manifestPath)}`);
223227

224-
selection = {data, manifestPath, localEnv, envFilePath};
228+
selection = {data, manifestPath};
225229
}
226230

227-
if (selection === null)
228-
return {type: `NoProject`, target: path.join(initialCwd, `package.json`)};
231+
if (localEnv)
232+
process.env = localEnv.env;
229233

230-
let envFilePath: string | undefined;
231-
if (selection.localEnv !== process.env) {
232-
envFilePath = selection.envFilePath;
233-
process.env = selection.localEnv;
234-
}
234+
if (selection === null)
235+
return {type: `NoProject`, target: path.join(initialCwd, `package.json`), envFilePath: localEnv?.path};
235236

236237
const rawPmSpec = parsePackageJSON(selection.data);
237238
if (typeof rawPmSpec === `undefined`)
238-
return {type: `NoSpec`, target: selection.manifestPath};
239+
return {type: `NoSpec`, target: selection.manifestPath, envFilePath: localEnv?.path};
239240

240241
debugUtils.log(`${selection.manifestPath} defines ${rawPmSpec} as local package manager`);
241242

242243
return {
243244
type: `Found`,
244245
target: selection.manifestPath,
245-
envFilePath,
246+
envFilePath: localEnv?.path,
246247
range: selection.data.devEngines?.packageManager?.version && {
247248
name: selection.data.devEngines.packageManager.name,
248249
range: selection.data.devEngines.packageManager.version,

‎tests/main.test.ts‎

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1322,6 +1322,95 @@ it(`should download latest pnpm from custom registry`, async () => {
13221322
});
13231323
});
13241324

1325+
it(`should use COREPACK_NPM_REGISTRY from .corepack.env for "corepack use" command`, async () => {
1326+
process.env.COREPACK_ENABLE_NETWORK = `0`;
1327+
1328+
await xfs.mktempPromise(async cwd => {
1329+
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {});
1330+
1331+
// Set COREPACK_NPM_REGISTRY in .corepack.env
1332+
await xfs.writeFilePromise(ppath.join(cwd, `.corepack.env` as Filename), `COREPACK_NPM_REGISTRY=http://custom-registry.example.com\n`);
1333+
1334+
// "corepack use pnpm" should read .corepack.env and use the custom registry
1335+
// When network is disabled, the error message should contain the custom registry URL
1336+
await expect(runCli(cwd, [`use`, `pnpm`])).resolves.toMatchObject({
1337+
stderr: ``,
1338+
stdout: expect.stringContaining(`custom-registry.example.com`),
1339+
exitCode: 1,
1340+
});
1341+
});
1342+
});
1343+
1344+
it(`should use closest .corepack.env`, async () => {
1345+
process.env.COREPACK_ENABLE_NETWORK = `0`;
1346+
process.env.DEBUG = `corepack`;
1347+
1348+
await xfs.mktempPromise(async cwd => {
1349+
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {
1350+
packageManager: `yarn@1.22.4+sha1.01c1197ca5b27f21edc8bc472cd4c8ce0e5a470e`,
1351+
});
1352+
1353+
// Set COREPACK_NPM_REGISTRY in .corepack.env
1354+
await xfs.writeFilePromise(ppath.join(cwd, `.corepack.env` as Filename), `COREPACK_NPM_REGISTRY=http://root.example.com\n`);
1355+
await xfs.mkdirPromise(ppath.join(cwd, `subdir`));
1356+
await xfs.writeFilePromise(ppath.join(cwd, `subdir`, `.corepack.env` as Filename), `COREPACK_NPM_REGISTRY=http://subdir.example.com\n`);
1357+
1358+
// "corepack yarn --version" should read .corepack.env and use the custom registry
1359+
// When network is disabled, the error message should contain the custom registry URL
1360+
await expect(runCli(ppath.join(cwd, `subdir`), [`yarn`, `--version`])).resolves.toMatchObject({
1361+
stdout: ``,
1362+
stderr: expect.stringContaining(`subdir.example.com`),
1363+
exitCode: 1,
1364+
});
1365+
});
1366+
});
1367+
1368+
it(`should ignore .corepack.env outside of the root`, async () => {
1369+
process.env.COREPACK_ENABLE_NETWORK = `0`;
1370+
process.env.DEBUG = `corepack`;
1371+
1372+
await xfs.mktempPromise(async cwd => {
1373+
// Set COREPACK_NPM_REGISTRY in a .corepack.env outside of the repo root
1374+
await xfs.writeFilePromise(ppath.join(cwd, `.corepack.env` as Filename), `COREPACK_NPM_REGISTRY=http://above-root.example.com\n`);
1375+
await xfs.mkdirPromise(ppath.join(cwd, `repo-root`));
1376+
await xfs.writeJsonPromise(ppath.join(cwd, `repo-root`, `package.json` as Filename), {
1377+
packageManager: `yarn@1.22.4+sha1.01c1197ca5b27f21edc8bc472cd4c8ce0e5a470e`,
1378+
});
1379+
1380+
// "corepack yarn --version" should NOT read .corepack.env and NOT use the custom registry
1381+
// When network is disabled, the error message should contain the custom registry URL
1382+
await expect(runCli(ppath.join(cwd, `repo-root`), [`yarn`, `--version`])).resolves.toMatchObject({
1383+
stdout: ``,
1384+
stderr: expect.not.stringContaining(`above-root.example.com`),
1385+
exitCode: 1,
1386+
});
1387+
});
1388+
});
1389+
1390+
it(`should ignore .corepack.env inside a node_modules folder`, async () => {
1391+
process.env.COREPACK_ENABLE_NETWORK = `0`;
1392+
process.env.DEBUG = `corepack`;
1393+
1394+
await xfs.mktempPromise(async cwd => {
1395+
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {
1396+
packageManager: `yarn@1.22.4+sha1.01c1197ca5b27f21edc8bc472cd4c8ce0e5a470e`,
1397+
});
1398+
1399+
// Set COREPACK_NPM_REGISTRY in a .corepack.env from a node_modules package
1400+
await xfs.mkdirPromise(ppath.join(cwd, `node_modules`));
1401+
await xfs.mkdirPromise(ppath.join(cwd, `node_modules`, `pkg`));
1402+
await xfs.writeFilePromise(ppath.join(cwd, `node_modules`, `pkg`, `.corepack.env` as Filename), `COREPACK_NPM_REGISTRY=http://npm-pkg.example.com\n`);
1403+
1404+
// "corepack yarn --version" should NOT read .corepack.env and NOT use the custom registry
1405+
// When network is disabled, the error message should contain the custom registry URL
1406+
await expect(runCli(ppath.join(cwd, `node_modules`, `pkg`), [`yarn`, `--version`])).resolves.toMatchObject({
1407+
stdout: ``,
1408+
stderr: expect.not.stringContaining(`npm-pkg.example.com`),
1409+
exitCode: 1,
1410+
});
1411+
});
1412+
});
1413+
13251414
describe(`should pick up COREPACK_INTEGRITY_KEYS from env`, () => {
13261415
beforeEach(() => {
13271416
process.env.AUTH_TYPE = `COREPACK_NPM_TOKEN`; // See `_registryServer.mjs`

0 commit comments

Comments
 (0)