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

## Utility Commands

Add `--verbose` to a Corepack utility command to print diagnostics on stderr,
for example `corepack cache clean --verbose`. Cache cleaning reports its target
directory and whether that cache exists; other commands include their package
manager resolution and download diagnostics. Normal stdout is unchanged.

To enable Corepack diagnostics when running a package manager, put the option
before its name: `corepack --verbose pnpm install`. Arguments after the package
manager name are passed to that manager. Setting `DEBUG=corepack` remains
supported.

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

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

import type {PreparedPackageManagerInfo} from '../Engine.ts';
import * as corepackUtils from '../corepackUtils.ts';
import * as debugUtils from '../debugUtils.ts';
import type {Context} from '../main.ts';
import * as specUtils from '../specUtils.ts';

export abstract class BaseCommand extends Command<Context> {
verbose = Option.Boolean(`--verbose`, false, {
description: `Print Corepack diagnostic information on stderr`,
});

async validateAndExecute() {
const wasEnabled = debugUtils.log.enabled;
if (this.verbose)
debugUtils.log.enabled = true;

try {
if (this.path.length > 0)
debugUtils.log(`Running ${this.path.join(` `)}`);

return await super.validateAndExecute();
} finally {
if (this.verbose) {
debugUtils.log.enabled = wasEnabled;
}
}
}

async resolvePatternsToDescriptors({patterns}: {patterns: Array<string>}) {
const resolvedSpecs = patterns.map(pattern => specUtils.parseSpec(pattern, `CLI arguments`, {enforceExactVersion: false}));

Expand Down
22 changes: 19 additions & 3 deletions sources/commands/Cache.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import {Command} from 'clipanion';
import fs from 'fs';

import * as debugUtils from '../debugUtils.ts';
import {getInstallFolder} from '../folderUtils.ts';
import type {Context} from '../main.ts';
import {isNodeError} from '../nodeUtils.ts';

export class CacheCommand extends Command<Context> {
import {BaseCommand} from './Base.ts';

export class CacheCommand extends BaseCommand {
static paths = [
[`cache`, `clean`],
[`cache`, `clear`],
Expand All @@ -18,6 +21,19 @@ export class CacheCommand extends Command<Context> {
});

async execute() {
await fs.promises.rm(getInstallFolder(), {recursive: true, force: true});
const folder = getInstallFolder();
try {
await fs.promises.lstat(folder);
} catch (error) {
if (isNodeError(error) && error.code === `ENOENT`) {
debugUtils.log(`Package manager cache does not exist at ${folder}`);
return;
}
throw error;
}

debugUtils.log(`Removing package manager cache at ${folder}`);
await fs.promises.rm(folder, {recursive: true, force: true});
debugUtils.log(`Finished cleaning package manager cache at ${folder}`);
}
}
5 changes: 3 additions & 2 deletions sources/commands/Disable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import path from 'p
import which from 'which';

import * as corepackUtils from '../corepackUtils.ts';
import type {Context} from '../main.ts';
import type {NodeError} from '../nodeUtils.ts';
import {isSupportedPackageManager, SupportedPackageManagerSetWithoutNpm} from '../types.ts';

export class DisableCommand extends Command<Context> {
import {BaseCommand} from './Base.ts';

export class DisableCommand extends BaseCommand {
static paths = [
[`disable`],
];
Expand Down
5 changes: 3 additions & 2 deletions sources/commands/Enable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import path from 'p
import which from 'which';

import * as corepackUtils from '../corepackUtils.ts';
import type {Context} from '../main.ts';
import {isSupportedPackageManager, SupportedPackageManagerSetWithoutNpm} from '../types.ts';

export class EnableCommand extends Command<Context> {
import {BaseCommand} from './Base.ts';

export class EnableCommand extends BaseCommand {
static paths = [
[`enable`],
];
Expand Down
31 changes: 31 additions & 0 deletions sources/commands/Verbose.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {Command, Option} from 'clipanion';

import {runMain} from '../main.ts';

import {BaseCommand} from './Base.ts';

export class VerboseCommand extends BaseCommand {
static paths = [[`--verbose`]];

static usage = Command.Usage({
description: `Run a command with Corepack diagnostics`,
details: `
Place \`--verbose\` before a package manager name to print Corepack
diagnostics on stderr. Arguments after the package manager name are
passed to that manager.
`,
examples: [[
`Show Corepack diagnostics when running pnpm`,
`corepack --verbose pnpm install`,
]],
});

// The flag is consumed as this command's path.
verbose = true;

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

async execute() {
await runMain(this.command);
}
}
14 changes: 7 additions & 7 deletions sources/commands/deprecated/Hydrate.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import {Command, Option, UsageError} from 'clipanion';
import {mkdir} from 'fs/promises';
import path from 'path';
import {Option, UsageError} from 'clipanion';
import {mkdir} from 'fs/promises';
import path from 'path';

import * as folderUtils from '../../folderUtils.ts';
import type {Context} from '../../main.ts';
import {isSupportedPackageManager} from '../../types.ts';
import * as folderUtils from '../../folderUtils.ts';
import {isSupportedPackageManager} from '../../types.ts';
import {BaseCommand} from '../Base.ts';

export class HydrateCommand extends Command<Context> {
export class HydrateCommand extends BaseCommand {
static paths = [
[`hydrate`],
];
Expand Down
16 changes: 8 additions & 8 deletions sources/commands/deprecated/Prepare.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import {Command, Option, UsageError} from 'clipanion';
import {mkdir} from 'fs/promises';
import path from 'path';
import {Option, UsageError} from 'clipanion';
import {mkdir} from 'fs/promises';
import path from 'path';

import * as folderUtils from '../../folderUtils.ts';
import type {Context} from '../../main.ts';
import * as specUtils from '../../specUtils.ts';
import type {Descriptor} from '../../types.ts';
import * as folderUtils from '../../folderUtils.ts';
import * as specUtils from '../../specUtils.ts';
import type {Descriptor} from '../../types.ts';
import {BaseCommand} from '../Base.ts';

export class PrepareCommand extends Command<Context> {
export class PrepareCommand extends BaseCommand {
static paths = [
[`prepare`],
];
Expand Down
2 changes: 2 additions & 0 deletions sources/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {InstallLocalCommand} from './commands/InstallLocal.ts';
import {PackCommand} from './commands/Pack.ts';
import {UpCommand} from './commands/Up.ts';
import {UseCommand} from './commands/Use.ts';
import {VerboseCommand} from './commands/Verbose.ts';
import {HydrateCommand} from './commands/deprecated/Hydrate.ts';
import {PrepareCommand} from './commands/deprecated/Prepare.ts';

Expand Down Expand Up @@ -70,6 +71,7 @@ export async function runMain(argv: Array<string>) {
cli.register(PackCommand);
cli.register(UpCommand);
cli.register(UseCommand);
cli.register(VerboseCommand);

// Deprecated commands
cli.register(HydrateCommand);
Expand Down
2 changes: 1 addition & 1 deletion tests/Use.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ describe(`UseCommand`, () => {
await expect(runCli(cwd, [`use`, `yarn@1.22.4`])).resolves.toMatchObject({
exitCode: 1,
stderr: ``,
stdout: `Installing yarn@1.22.4 in the project...\nUsage Error: The requested version of yarn@1.22.4+sha512.a1833b862fe52169bd6c2a033045a07df5bc6a23595c259e675fed1b2d035ab37abe6ce309720abb6636d68f03615054b6292dc0a70da31c8697fda228b50d18 does not match the devEngines specification (yarn@2.x)\n\n$ corepack use <pattern>\n`,
stdout: `Installing yarn@1.22.4 in the project...\nUsage Error: The requested version of yarn@1.22.4+sha512.a1833b862fe52169bd6c2a033045a07df5bc6a23595c259e675fed1b2d035ab37abe6ce309720abb6636d68f03615054b6292dc0a70da31c8697fda228b50d18 does not match the devEngines specification (yarn@2.x)\n\n$ corepack use [--verbose] <pattern>\n`,
});

// Should accept setting to a compatible version:
Expand Down
134 changes: 134 additions & 0 deletions tests/Verbose.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import {npath, ppath, xfs} from '@yarnpkg/fslib';
import type {PortablePath} 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.COREPACK_ENABLE_NETWORK = `0`;
return () => xfs.removePromise(home, {recursive: true});
});

async function cachePackageManager(cwd: PortablePath) {
await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), {packageManager: `pnpm@1.0.0`});
const folder = ppath.join(npath.toPortablePath(process.env.COREPACK_HOME!), `v1`, `pnpm`, `1.0.0`);
await xfs.mkdirpPromise(folder);
await xfs.writeJsonPromise(ppath.join(folder, `.corepack`), {
locator: {name: `pnpm`, reference: `1.0.0`},
bin: {pnpm: `pnpm.js`},
hash: `sha512.abc`,
});
await xfs.writeFilePromise(ppath.join(folder, `pnpm.js`), `console.log(JSON.stringify(process.argv.slice(2))); if (process.argv.includes('--fail')) process.exitCode = 23;\n`);
}

describe(`verbose diagnostics`, () => {
it(`keeps the global command help available`, async () => {
await xfs.mktempPromise(async cwd => {
const result = await runCli(cwd, [`--help`]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain(`corepack cache clean`);
expect(result.stdout).toContain(`corepack use`);
expect(result.stderr).toBe(``);
});
});

it(`supports the version command`, async () => {
await xfs.mktempPromise(async cwd => {
const plain = await runCli(cwd, [`--version`]);
const verbose = await runCli(cwd, [`--verbose`, `--version`]);
expect(verbose.exitCode).toBe(0);
expect(verbose.stdout).toBe(plain.stdout);
});
});

it.each([
{command: [`cache`, `clean`], verbose: false},
{command: [`cache`, `clear`, `--no-verbose`], verbose: false},
{command: [`cache`, `clean`, `--verbose`], verbose: true},
{command: [`--verbose`, `cache`, `clear`], verbose: true},
])(`cleans the cache with $command`, async ({command, verbose}) => {
await xfs.mktempPromise(async cwd => {
const folder = ppath.join(npath.toPortablePath(process.env.COREPACK_HOME!), `v1`);
await xfs.mkdirpPromise(folder);
await xfs.writeFilePromise(ppath.join(folder, `cached-file`), `cached`);

const result = await runCli(cwd, command);
expect(result.exitCode).toBe(0);
expect(result.stdout).toBe(``);
if (verbose) {
expect(result.stderr).toContain(`Removing package manager cache at ${npath.fromPortablePath(folder)}`);
expect(result.stderr).toContain(`Finished cleaning package manager cache`);
} else {
expect(result.stderr).toBe(``);
}
await expect(xfs.existsPromise(folder)).resolves.toBe(false);
});
});

it(`reports when the cache is already absent`, async () => {
await xfs.mktempPromise(async cwd => {
await expect(runCli(cwd, [`cache`, `clean`, `--verbose`])).resolves.toMatchObject({
exitCode: 0,
stdout: ``,
stderr: expect.stringContaining(`Package manager cache does not exist at`),
});
});
});

it(`preserves DEBUG-based diagnostics`, async () => {
process.env.DEBUG = `corepack`;
await xfs.mktempPromise(async cwd => {
await expect(runCli(cwd, [`cache`, `clean`])).resolves.toMatchObject({
exitCode: 0,
stdout: ``,
stderr: expect.stringContaining(`Package manager cache does not exist at`),
});
});
});

it.each([true, false])(`forwards package manager arguments (Corepack verbose: %s)`, async verbose => {
await xfs.mktempPromise(async cwd => {
await cachePackageManager(cwd);
const args = [`--verbose`, `--filter`, `@scope/example`, `install`];
const result = await runCli(cwd, [...(verbose ? [`--verbose`] : []), `pnpm`, ...args]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toBe(`${JSON.stringify(args)}\n`);
if (verbose) {
expect(result.stderr).toContain(`corepack`);
expect(result.stderr).toContain(`Reusing pnpm@1.0.0`);
} else {
expect(result.stderr).toBe(``);
}
});
});

it(`preserves a package manager's exit code`, async () => {
await xfs.mktempPromise(async cwd => {
await cachePackageManager(cwd);
await expect(runCli(cwd, [`--verbose`, `pnpm`, `--fail`])).resolves.toMatchObject({exitCode: 23});
});
});

it(`preserves utility command failures`, async () => {
await xfs.mktempPromise(async cwd => {
await expect(runCli(cwd, [`--verbose`, `not-a-command`])).resolves.toMatchObject({exitCode: 1});
});
});

it(`keeps --json output unchanged`, async () => {
await xfs.mktempPromise(async cwd => {
await cachePackageManager(cwd);
const args = [`pack`, `pnpm@1.0.0`, `--json`];
const plain = await runCli(cwd, args);
const verbose = await runCli(cwd, [...args, `--verbose`]);
expect(plain.exitCode).toBe(0);
expect(verbose.exitCode).toBe(0);
expect(verbose.stdout).toBe(plain.stdout);
expect(verbose.stderr).toContain(`Reusing pnpm@1.0.0`);
await expect(xfs.existsPromise(ppath.join(cwd, `corepack.tgz`))).resolves.toBe(true);
});
});
});