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
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,18 @@ Package managers thus installed will be configured as the new default when
calling their respective binaries outside of projects defining the
`packageManager` field.

### `corepack list <-g,--global>`

List the versions of npm, pnpm, and Yarn installed in the local `COREPACK_HOME`
cache, sorted by name and then by version (newest first). `corepack ls -g` and
`corepack -g ls` are equivalent. Add `--json` to print an array of objects with
`name`, `version`, and `isDefault` fields.

The recorded global default from `lastKnownGood.json` is marked with `(default)`.
Only cached versions are listed; an uncached default is not included. This command
does not contact the registry, install anything, or update the defaults. It does
not report package managers installed outside Corepack or the project's selection.

### `corepack pack [... name[@<version>]]`

| Option | Description |
Expand Down
90 changes: 90 additions & 0 deletions sources/commands/List.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import {Command, Option} from 'clipanion';
import fs from 'fs';
import path from 'path';
import semverParse from 'semver/functions/parse.js';
import semverRcompare from 'semver/functions/rcompare.js';

import {getLastKnownGood} from '../Engine.ts';
import {getInstallFolder} from '../folderUtils.ts';
import type {Context} from '../main.ts';
import {isNodeError} from '../nodeUtils.ts';
import {loadSpecAndEnv} from '../specUtils.ts';
import {SupportedPackageManagerSet} from '../types.ts';

export class ListCommand extends Command<Context> {
static paths = [
[`list`],
[`ls`],
];

static usage = Command.Usage({
description: `List installed package managers`,
details: `
List locally cached versions of the supported package managers, marking
each recorded global default with \`(default)\`. This command does not
access the network or change the cache.
`,
examples: [[
`List installed package managers and their recorded defaults`,
`corepack list -g`,
]],
});

global = Option.Boolean(`-g,--global`, {required: true});

json = Option.Boolean(`--json`, false, {
description: `Print the installed versions as a JSON array`,
});

async execute() {
await loadSpecAndEnv(this.context.cwd, {envOnly: true});
const lastKnownGood = await getLastKnownGood();
const installed: Array<{name: string, version: string, isDefault: boolean}> = [];

for (const name of SupportedPackageManagerSet) {
const folder = path.join(getInstallFolder(), name);
let entries;
try {
entries = await fs.promises.readdir(folder, {withFileTypes: true});
} catch (error) {
if (isNodeError(error) && error.code === `ENOENT`)
continue;
throw error;
}

const defaultVersion = semverParse(lastKnownGood[name])?.version;
for (const entry of entries) {
if (!entry.isDirectory() || !semverParse(entry.name))
continue;

// The metadata file is written only after a successful download.
let metadata;
try {
metadata = JSON.parse(await fs.promises.readFile(path.join(folder, entry.name, `.corepack`), `utf8`));
} catch (error) {
if (error instanceof SyntaxError || (isNodeError(error) && error.code === `ENOENT`))
continue;
throw error;
}

const version = semverParse(metadata?.locator?.reference)?.version;
if (metadata?.locator?.name !== name || version !== entry.name)
continue;

installed.push({name, version, isDefault: version === defaultVersion});
}
}

installed.sort((a, b) => a.name.localeCompare(b.name) || semverRcompare(a.version, b.version));

if (this.json) {
this.context.stdout.write(`${JSON.stringify(installed)}\n`);
} else if (installed.length === 0) {
this.context.stdout.write(`No package managers are installed in the Corepack cache.\n`);
} else {
for (const {name, version, isDefault} of installed) {
this.context.stdout.write(`${name}@${version}${isDefault ? ` (default)` : ``}\n`);
}
}
}
}
2 changes: 2 additions & 0 deletions sources/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {DisableCommand} from './commands/Disable.ts';
import {EnableCommand} from './commands/Enable.ts';
import {InstallGlobalCommand} from './commands/InstallGlobal.ts';
import {InstallLocalCommand} from './commands/InstallLocal.ts';
import {ListCommand} from './commands/List.ts';
import {PackCommand} from './commands/Pack.ts';
import {UpCommand} from './commands/Up.ts';
import {UseCommand} from './commands/Use.ts';
Expand Down Expand Up @@ -67,6 +68,7 @@ export async function runMain(argv: Array<string>) {
cli.register(EnableCommand);
cli.register(InstallGlobalCommand);
cli.register(InstallLocalCommand);
cli.register(ListCommand);
cli.register(PackCommand);
cli.register(UpCommand);
cli.register(UseCommand);
Expand Down
136 changes: 136 additions & 0 deletions tests/List.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import type {Filename} from '@yarnpkg/fslib';
import {ppath, xfs, npath} from '@yarnpkg/fslib';
import process from 'node:process';
import {describe, beforeEach, it, expect} from 'vitest';

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

beforeEach(async () => {
const home = await xfs.mktempPromise();
process.env.COREPACK_HOME = npath.fromPortablePath(home);
process.env.COREPACK_ENABLE_NETWORK = `0`;
return async () => {
await xfs.removePromise(home, {recursive: true});
};
});

function getHome() {
return npath.toPortablePath(process.env.COREPACK_HOME!);
}

async function cacheVersion(name: string, reference: string) {
const folder = ppath.join(getHome(), `v1`, name as Filename, reference.split(`+`)[0] as Filename);
await xfs.mkdirpPromise(folder);
await xfs.writeJsonPromise(ppath.join(folder, `.corepack`), {
locator: {name, reference},
bin: {[name]: `bin.js`},
hash: `sha512.deadbeef`,
});
return folder;
}

describe(`ListCommand`, () => {
it.each([
[`list`, `-g`],
[`ls`, `--global`],
[`-g`, `ls`],
])(`supports %j`, async (...args) => {
await cacheVersion(`pnpm`, `9.10.0`);
await cacheVersion(`pnpm`, `9.9.0`);
await cacheVersion(`npm`, `10.0.0`);
await cacheVersion(`yarn`, `4.0.0-rc.1`);
await cacheVersion(`yarn`, `4.0.0`);
await xfs.writeJsonPromise(ppath.join(getHome(), `lastKnownGood.json`), {
pnpm: `9.9.0+sha512.deadbeef`,
});

await expect(runCli(getHome(), args)).resolves.toEqual({
exitCode: 0,
stdout: `npm@10.0.0\npnpm@9.10.0\npnpm@9.9.0 (default)\nyarn@4.0.0\nyarn@4.0.0-rc.1\n`,
stderr: ``,
});
});

it(`lists installed versions as JSON without modifying defaults or the project`, async () => {
await cacheVersion(`pnpm`, `9.10.0+sha512.deadbeef`);
const defaults = {pnpm: `9.10.0`, yarn: `4.0.0`};
const manifest = {packageManager: `yarn@3.0.0`};
await xfs.writeJsonPromise(ppath.join(getHome(), `lastKnownGood.json`), defaults);
await xfs.writeJsonPromise(ppath.join(getHome(), `package.json`), manifest);

const result = await runCli(getHome(), [`list`, `-g`, `--json`]);
expect(result).toMatchObject({exitCode: 0, stderr: ``});
expect(JSON.parse(result.stdout)).toEqual([{name: `pnpm`, version: `9.10.0`, isDefault: true}]);
await expect(xfs.readJsonPromise(ppath.join(getHome(), `lastKnownGood.json`))).resolves.toEqual(defaults);
await expect(xfs.readJsonPromise(ppath.join(getHome(), `package.json`))).resolves.toEqual(manifest);
});

it(`does not create a missing cache`, async () => {
const cwd = getHome();
process.env.COREPACK_HOME = npath.fromPortablePath(ppath.join(cwd, `missing`));
await expect(runCli(cwd, [`list`, `-g`, `--json`])).resolves.toEqual({
exitCode: 0, stdout: `[]\n`, stderr: ``,
});
await expect(xfs.existsPromise(getHome())).resolves.toBe(false);
});

it(`loads COREPACK_HOME from the closest environment file`, async () => {
await cacheVersion(`pnpm`, `9.0.0`);
const cwd = getHome();
await xfs.writeFilePromise(ppath.join(cwd, `.corepack.env`), `COREPACK_HOME=${JSON.stringify(process.env.COREPACK_HOME)}\n`);
delete process.env.COREPACK_HOME;
await expect(runCli(cwd, [`list`, `-g`])).resolves.toEqual({
exitCode: 0, stdout: `pnpm@9.0.0\n`, stderr: ``,
});
});

it(`explains an empty cache`, async () => {
await expect(runCli(getHome(), [`list`, `-g`])).resolves.toEqual({
exitCode: 0,
stdout: `No package managers are installed in the Corepack cache.\n`,
stderr: ``,
});
});

it(`ignores temporary, incomplete, and invalid cache entries`, async () => {
await cacheVersion(`pnpm`, `9.0.0`);
const folder = ppath.join(getHome(), `v1`, `pnpm`);
await xfs.mkdirPromise(ppath.join(folder, `10.0.0`));
await xfs.mkdirPromise(ppath.join(folder, `.DS_Store`));
await xfs.writeFilePromise(ppath.join(folder, `11.0.0`), ``);
for (const [version, content] of [
[`12.0.0`, `{`],
[`13.0.0`, `null`],
[`14.0.0`, JSON.stringify({locator: {name: `yarn`, reference: `14.0.0`}})],
[`15.0.0`, JSON.stringify({locator: {name: `pnpm`, reference: `16.0.0`}})],
]) {
const entry = await cacheVersion(`pnpm`, version);
await xfs.writeFilePromise(ppath.join(entry, `.corepack`), content);
}
await expect(runCli(getHome(), [`list`, `-g`])).resolves.toEqual({
exitCode: 0, stdout: `pnpm@9.0.0\n`, stderr: ``,
});
});

it(`tolerates invalid Last Known Good data`, async () => {
await cacheVersion(`pnpm`, `9.0.0`);
await xfs.writeFilePromise(ppath.join(getHome(), `lastKnownGood.json`), `{`);
await expect(runCli(getHome(), [`list`, `-g`])).resolves.toEqual({
exitCode: 0, stdout: `pnpm@9.0.0\n`, stderr: ``,
});
});

it(`lists versions installed by Corepack and updates after cache clean`, async () => {
delete process.env.COREPACK_ENABLE_NETWORK;
process.env.COREPACK_DEFAULT_TO_LATEST = `0`;
await expect(runCli(getHome(), [`install`, `-g`, `yarn@1.22.4`])).resolves.toMatchObject({exitCode: 0});
process.env.COREPACK_ENABLE_NETWORK = `0`;
await expect(runCli(getHome(), [`ls`, `-g`])).resolves.toEqual({
exitCode: 0, stdout: `yarn@1.22.4 (default)\n`, stderr: ``,
});
await expect(runCli(getHome(), [`cache`, `clean`])).resolves.toMatchObject({exitCode: 0});
await expect(runCli(getHome(), [`ls`, `-g`, `--json`])).resolves.toEqual({
exitCode: 0, stdout: `[]\n`, stderr: ``,
});
});
});