Skip to content
Closed
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
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,20 @@ The utility commands detailed in the next section.

## Utility Commands

### `corepack --dir <path> <command> [... args]`

Run a Corepack command or package manager from the specified directory. Relative
paths are resolved from the current directory; `--dir=<path>` is also supported.
Place this option before the command, for example:

```sh
corepack --dir ./my-project install
corepack --dir ./my-project pnpm install
```

Corepack looks up the project's configuration in that directory, and the package
manager runs there. Arguments after the package manager name are passed to it.

### `corepack <binary name>[@<version>] [... args]`

This meta-command runs the specified package manager in the local folder. You
Expand Down
48 changes: 48 additions & 0 deletions sources/commands/Dir.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {Command, Option, UsageError} from 'clipanion';
import path from 'path';

import type {Context} from '../main.ts';
import {runMain} from '../main.ts';
import {isNodeError} from '../nodeUtils.ts';

export class DirCommand extends Command<Context> {
static paths = [Command.Default];

static usage = Command.Usage({
description: `Run a command from another directory`,
details: `
Place \`--dir <path>\` before the command to run it from the specified
directory. Relative paths are resolved from the current directory.

This applies to both Corepack commands and package manager invocations.
Arguments after the package manager name are passed to that manager.
`,
examples: [[
`Install another project's package manager`,
`corepack --dir ./my-project install`,
], [
`Run pnpm in another project`,
`corepack --dir ./my-project pnpm install`,
]],
});

directory = Option.String(`--dir`, {
required: true,
description: `The directory in which to run the command`,
});

command = Option.Proxy({name: `command`, required: 1});

async execute() {
const directory = path.resolve(this.context.cwd, this.directory);
try {
process.chdir(directory);
} catch (error) {
if (isNodeError(error))
throw new UsageError(`Couldn't change directory to ${JSON.stringify(directory)}: ${error.message}`);
throw error;
}

await runMain(this.command);
}
}
4 changes: 3 additions & 1 deletion sources/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import pJSON from '../package.json' with {type: 'json'}
import type {PackageManagerRequest} from './Engine.ts';
import {Engine} from './Engine.ts';
import {CacheCommand} from './commands/Cache.ts';
import {DirCommand} from './commands/Dir.ts';
import {DisableCommand} from './commands/Disable.ts';
import {EnableCommand} from './commands/Enable.ts';
import {InstallGlobalCommand} from './commands/InstallGlobal.ts';
Expand All @@ -22,7 +23,7 @@ export type Context = BaseContext & CustomContext;
const {version: corepackVersion} = pJSON;

function getPackageManagerRequestFromCli(parameter: string | undefined, engine: Engine): PackageManagerRequest | null {
if (!parameter)
if (!parameter || parameter.startsWith(`-`))
return null;

const match = parameter.match(/^([^@]*)(?:@(.*))?$/);
Expand Down Expand Up @@ -64,6 +65,7 @@ export async function runMain(argv: Array<string>) {

cli.register(CacheCommand);
cli.register(DisableCommand);
cli.register(DirCommand);
cli.register(EnableCommand);
cli.register(InstallGlobalCommand);
cli.register(InstallLocalCommand);
Expand Down
125 changes: 125 additions & 0 deletions tests/Dir.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import {npath, ppath, xfs} from '@yarnpkg/fslib';
import {beforeEach, describe, expect, it} from 'vitest';

import {runCli} from './_runCli.ts';

beforeEach(async () => {
const home = await xfs.mktempPromise();
process.env.COREPACK_HOME = npath.fromPortablePath(home);
process.env.COREPACK_DEFAULT_TO_LATEST = `0`;
process.env.AUTH_TYPE = `COREPACK_NPM_TOKEN`;
process.env.TEST_INTEGRITY = `valid`;

return async () => {
await xfs.removePromise(home, {recursive: true});
};
});

describe(`--dir`, () => {
it.each([
{absolute: false, bound: false},
{absolute: false, bound: true},
{absolute: true, bound: false},
{absolute: true, bound: true},
])(`uses the target manifest with absolute=$absolute, bound=$bound`, async ({absolute, bound}) => {
await xfs.mktempPromise(async cwd => {
const project = ppath.join(cwd, `project @scope`);
await xfs.mkdirPromise(project);
await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), {packageManager: `[email protected]`});
await xfs.writeJsonPromise(ppath.join(project, `package.json`), {packageManager: `[email protected]`});

const directory = absolute ? npath.fromPortablePath(project) : `project @scope`;
const options = bound ? [`--dir=${directory}`] : [`--dir`, directory];
await expect(runCli(cwd, [...options, `pnpm`, `--version`], true)).resolves.toEqual({
exitCode: 0,
stdout: `pnpm: Hello from custom registry\n`,
stderr: ``,
});
});
});

it(`runs Corepack utility commands in the target project`, async () => {
await xfs.mktempPromise(async cwd => {
const project = ppath.join(cwd, `project`);
await xfs.mkdirPromise(project);
await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), {packageManager: `[email protected]`});
await xfs.writeJsonPromise(ppath.join(project, `package.json`), {packageManager: `[email protected]`});

await expect(runCli(cwd, [`--dir`, `project`, `install`], true)).resolves.toEqual({
exitCode: 0,
stdout: `Adding [email protected] to the cache...\n`,
stderr: ``,
});

process.env.COREPACK_ENABLE_NETWORK = `0`;
await expect(runCli(cwd, [`--dir`, `project`, `pnpm`, `--version`], true)).resolves.toEqual({
exitCode: 0,
stdout: `pnpm: Hello from custom registry\n`,
stderr: ``,
});
});
});

it(`changes the executed process cwd and forwards the package manager's own --dir`, async () => {
process.env.CI = `1`;

await xfs.mktempPromise(async cwd => {
const project = ppath.join(cwd, `project`);
const nested = ppath.join(project, `nested`);
await xfs.mkdirPromise(nested, {recursive: true});
await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), {packageManager: `[email protected]`});
await xfs.writeJsonPromise(ppath.join(project, `package.json`), {packageManager: `[email protected]`});
await xfs.writeJsonPromise(ppath.join(nested, `package.json`), {private: true});
await xfs.writeFilePromise(ppath.join(nested, `print-cwd.cjs`), `console.log(process.cwd());\n`);

await expect(runCli(cwd, [`--dir`, `project`, `pnpm`, `--dir`, `nested`, `exec`, `node`, `print-cwd.cjs`])).resolves.toEqual({
exitCode: 0,
stdout: `${npath.fromPortablePath(nested)}\n`,
stderr: ``,
});
});
});

it(`loads the target .corepack.env and propagates command failures`, async () => {
await xfs.mktempPromise(async cwd => {
const project = ppath.join(cwd, `project`);
await xfs.mkdirPromise(project);
await xfs.writeFilePromise(ppath.join(project, `.corepack.env`), `COREPACK_ENABLE_NETWORK=0\n`);

await expect(runCli(cwd, [`--dir`, `project`, `use`, `pnpm@latest`], true)).resolves.toMatchObject({
exitCode: 1,
stdout: expect.stringContaining(`Network access disabled by the environment`),
stderr: ``,
});
});
});

it.each([false, true])(`reports an invalid directory with existing file=%s`, async existingFile => {
await xfs.mktempPromise(async cwd => {
if (existingFile)
await xfs.writeFilePromise(ppath.join(cwd, `invalid`), ``);

await expect(runCli(cwd, [`--dir`, `invalid`, `install`])).resolves.toMatchObject({
exitCode: 1,
stdout: expect.stringContaining(`Couldn't change directory to`),
stderr: ``,
});
});
});

it.each([[`--dir`], [`--dir`, `.`]])(`rejects incomplete arguments %j`, async (...args) => {
await xfs.mktempPromise(async cwd => {
await expect(runCli(cwd, args)).resolves.toMatchObject({exitCode: 1});
});
});

it(`keeps the other commands visible in the main help`, async () => {
await xfs.mktempPromise(async cwd => {
const result = await runCli(cwd, [`--help`]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain(`corepack use`);
expect(result.stdout).toContain(`corepack up`);
expect(result.stdout).toContain(`--dir`);
});
});
});