Skip to content
Merged
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
2 changes: 1 addition & 1 deletion sources/Engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`: {
Expand Down
4 changes: 3 additions & 1 deletion sources/commands/Base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export abstract class BaseCommand extends Command<Context> {
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`);
Expand All @@ -22,6 +22,8 @@ export abstract class BaseCommand extends Command<Context> {
return [lookup.range ?? lookup.getSpec()];
}
}
} else {
await specUtils.loadSpecAndEnv(this.context.cwd, {envOnly: true});
}

return resolvedSpecs;
Expand Down
4 changes: 3 additions & 1 deletion sources/commands/deprecated/Prepare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export class PrepareCommand extends Command<Context> {
const installLocations: Array<string> = [];

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`);
Expand All @@ -45,6 +45,8 @@ export class PrepareCommand extends Command<Context> {
specs.push(lookup.getSpec());
}
}
} else {
await specUtils.loadSpecAndEnv(this.context.cwd, {envOnly: true});
}

for (const request of specs) {
Expand Down
87 changes: 44 additions & 43 deletions sources/specUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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<LoadSpecResult> {
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<LoadSpecResult> {
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;
Expand All @@ -177,6 +199,14 @@ export async function loadSpec(initialCwd: string): Promise<LoadSpecResult> {
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;
Expand All @@ -193,56 +223,27 @@ export async function loadSpec(initialCwd: string): Promise<LoadSpecResult> {
} 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,
Expand Down
89 changes: 89 additions & 0 deletions tests/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down