Files
bun.sh/test/cli/install/bun-pm.test.ts
robobun 476e1cfe69 Make 'bun list' an alias for 'bun pm ls' (#24159)
## Summary

This PR makes `bun list` an alias for `bun pm ls`, allowing users to
list their dependency tree with a shorter command.

## Changes

- Updated `src/cli.zig` to route `list` command to
`PackageManagerCommand` instead of `ReservedCommand`
- Modified `src/cli/package_manager_command.zig` to detect when `bun
list` is invoked directly and treat it as `ls`
- Updated help text in `bun pm --help` to show both `bun list` and `bun
pm ls` as valid options

## Implementation Details

The implementation follows the same pattern used for `bun whoami`, which
is also a direct alias to a pm subcommand. When `bun list` is detected,
it's internally converted to the `ls` subcommand.

## Testing

Tested locally:
-  `bun list` shows the dependency tree
-  `bun list --all` works correctly with the `--all` flag
-  `bun pm ls` continues to work (backward compatible)

## Test Output

```bash
$ bun list
/tmp/test-bun-list node_modules (3)
└── react@18.3.1

$ bun list --all
/tmp/test-bun-list node_modules
├── js-tokens@4.0.0
├── loose-envify@1.4.0
└── react@18.3.1

$ bun pm ls
/tmp/test-bun-list node_modules (3)
└── react@18.3.1
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-29 21:05:25 -07:00

589 lines
14 KiB
TypeScript

import { spawn } from "bun";
import { afterAll, afterEach, beforeAll, beforeEach, expect, it, test } from "bun:test";
import { exists, mkdir, writeFile } from "fs/promises";
import { bunEnv, bunExe, bunEnv as env, readdirSorted, tmpdirSync } from "harness";
import { cpSync } from "node:fs";
import { join } from "path";
import {
dummyAfterAll,
dummyAfterEach,
dummyBeforeAll,
dummyBeforeEach,
dummyRegistry,
package_dir,
requested,
root_url,
setHandler,
} from "./dummy.registry";
beforeAll(dummyBeforeAll);
afterAll(dummyAfterAll);
beforeEach(async () => {
await dummyBeforeEach();
});
afterEach(dummyAfterEach);
it("should list top-level dependency", async () => {
const urls: string[] = [];
setHandler(dummyRegistry(urls));
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({
name: "foo",
version: "0.0.1",
dependencies: {
moo: "./moo",
},
}),
);
await mkdir(join(package_dir, "moo"));
await writeFile(
join(package_dir, "moo", "package.json"),
JSON.stringify({
name: "moo",
version: "0.1.0",
dependencies: {
bar: "latest",
},
}),
);
{
const { stderr, stdout, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const err = await stderr.text();
expect(err).not.toContain("error:");
expect(err).toContain("Saved lockfile");
expect(await exited).toBe(0);
}
expect(urls.sort()).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]);
expect(requested).toBe(2);
urls.length = 0;
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "pm", "ls"],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
expect(await stderr.text()).toBe("");
expect(await stdout.text()).toBe(`${package_dir} node_modules (2)
└── moo@moo
`);
expect(await exited).toBe(0);
expect(urls.sort()).toEqual([]);
expect(requested).toBe(2);
});
it("should list all dependencies", async () => {
const urls: string[] = [];
setHandler(dummyRegistry(urls));
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({
name: "foo",
version: "0.0.1",
dependencies: {
moo: "./moo",
},
}),
);
await mkdir(join(package_dir, "moo"));
await writeFile(
join(package_dir, "moo", "package.json"),
JSON.stringify({
name: "moo",
version: "0.1.0",
dependencies: {
bar: "latest",
},
}),
);
{
const { stderr, stdout, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const err = await stderr.text();
expect(err).not.toContain("error:");
expect(err).toContain("Saved lockfile");
expect(await exited).toBe(0);
}
expect(urls.sort()).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]);
expect(requested).toBe(2);
urls.length = 0;
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "pm", "ls", "--all"],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
expect(await stderr.text()).toBe("");
expect(await stdout.text()).toBe(`${package_dir} node_modules
├── bar@0.0.2
└── moo@moo
`);
expect(await exited).toBe(0);
expect(urls.sort()).toEqual([]);
expect(requested).toBe(2);
});
it("should list top-level aliased dependency", async () => {
const urls: string[] = [];
setHandler(dummyRegistry(urls));
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({
name: "foo",
version: "0.0.1",
dependencies: {
"moo-1": "./moo",
},
}),
);
await mkdir(join(package_dir, "moo"));
await writeFile(
join(package_dir, "moo", "package.json"),
JSON.stringify({
name: "moo",
version: "0.1.0",
dependencies: {
"bar-1": "npm:bar",
},
}),
);
{
const { stderr, stdout, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const err = await stderr.text();
expect(err).not.toContain("error:");
expect(err).toContain("Saved lockfile");
expect(await exited).toBe(0);
}
expect(urls.sort()).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]);
expect(requested).toBe(2);
urls.length = 0;
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "pm", "ls"],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
expect(await stderr.text()).toBe("");
expect(await stdout.text()).toBe(`${package_dir} node_modules (2)
└── moo-1@moo
`);
expect(await exited).toBe(0);
expect(urls.sort()).toEqual([]);
expect(requested).toBe(2);
});
it("should list aliased dependencies", async () => {
const urls: string[] = [];
setHandler(dummyRegistry(urls));
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({
name: "foo",
version: "0.0.1",
dependencies: {
"moo-1": "./moo",
},
}),
);
await mkdir(join(package_dir, "moo"));
await writeFile(
join(package_dir, "moo", "package.json"),
JSON.stringify({
name: "moo",
version: "0.1.0",
dependencies: {
"bar-1": "npm:bar",
},
}),
);
{
const { stderr, stdout, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const err = await stderr.text();
expect(err).not.toContain("error:");
expect(err).toContain("Saved lockfile");
expect(await exited).toBe(0);
}
expect(urls.sort()).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]);
expect(requested).toBe(2);
urls.length = 0;
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "pm", "ls", "--all"],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
expect(await stderr.text()).toBe("");
expect(await stdout.text()).toBe(`${package_dir} node_modules
├── bar-1@0.0.2
└── moo-1@moo
`);
expect(await exited).toBe(0);
expect(urls.sort()).toEqual([]);
expect(requested).toBe(2);
});
it("should remove all cache", async () => {
const urls: string[] = [];
setHandler(dummyRegistry(urls));
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({
name: "foo",
version: "0.0.1",
dependencies: {
"moo-1": "./moo",
},
}),
);
await mkdir(join(package_dir, "moo"));
await writeFile(
join(package_dir, "moo", "package.json"),
JSON.stringify({
name: "moo",
version: "0.1.0",
dependencies: {
"bar-1": "npm:bar",
},
}),
);
let cache_dir: string = join(package_dir, "node_modules", ".cache");
{
const { stderr, stdout, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env: {
...env,
BUN_INSTALL_CACHE_DIR: cache_dir,
},
});
const err = await stderr.text();
expect(err).not.toContain("error:");
expect(err).toContain("Saved lockfile");
expect(await exited).toBe(0);
}
expect(urls.sort()).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]);
expect(requested).toBe(2);
expect(await readdirSorted(cache_dir)).toContain("bar");
const {
stdout: stdout1,
stderr: stderr1,
exited: exited1,
} = spawn({
cmd: [bunExe(), "pm", "cache"],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env: {
...env,
BUN_INSTALL_CACHE_DIR: cache_dir,
},
});
expect(await new Response(stderr1).text()).toBe("");
expect(await new Response(stdout1).text()).toBe(cache_dir);
expect(await exited1).toBe(0);
const {
stdout: stdout2,
stderr: stderr2,
exited: exited2,
} = spawn({
cmd: [bunExe(), "pm", "cache", "rm"],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env: {
...env,
BUN_INSTALL_CACHE_DIR: cache_dir,
},
});
expect(await new Response(stderr2).text()).toBe("");
expect(await new Response(stdout2).text()).toInclude("Cleared 'bun install' cache\n");
expect(await exited2).toBe(0);
expect(await exists(cache_dir)).toBeFalse();
});
it("bun pm migrate", async () => {
const test_dir = tmpdirSync();
cpSync(join(import.meta.dir, "migration/contoso-test"), test_dir, { recursive: true });
const { stdout, stderr, exitCode } = Bun.spawnSync({
cmd: [bunExe(), "pm", "migrate", "--force"],
cwd: test_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env: bunEnv,
});
expect(exitCode).toBe(0);
expect(stdout.toString("utf-8")).toBe("");
expect(stderr.toString("utf-8")).toEndWith("migrated lockfile from package-lock.json\n");
const hashExec = Bun.spawnSync({
cmd: [bunExe(), "pm", "hash"],
cwd: test_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env: bunEnv,
});
expect(hashExec.exitCode).toBe(0);
const hash = hashExec.stdout.toString("utf-8").trim();
expect(hash).toMatchSnapshot();
});
test("bun whoami executes pm whoami", async () => {
// Test that "bun whoami" doesn't show reservation message and instead executes pm whoami
// First create a simple package.json
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({
name: "test-whoami",
version: "1.0.0",
}),
);
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "whoami"],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env: bunEnv,
});
const [stderrText, stdoutText, exitCode] = await Promise.all([
new Response(stderr).text(),
new Response(stdout).text(),
exited,
]);
// Should get authentication error instead of reservation message
expect(stderrText).toContain("missing authentication");
expect(stderrText).not.toContain("reserved for future use");
expect(stdoutText).not.toContain("reserved for future use");
// Exit code will be non-zero due to missing auth
expect(exitCode).toBe(1);
});
test("bun pm whoami still works", async () => {
// Test that "bun pm whoami" still works as expected
// First create a simple package.json
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({
name: "test-pm-whoami",
version: "1.0.0",
}),
);
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "pm", "whoami"],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env: bunEnv,
});
const [stderrText, stdoutText, exitCode] = await Promise.all([
new Response(stderr).text(),
new Response(stdout).text(),
exited,
]);
// Should get authentication error
expect(stderrText).toContain("missing authentication");
expect(stderrText).not.toContain("reserved for future use");
expect(stdoutText).not.toContain("reserved for future use");
// Exit code will be non-zero due to missing auth
expect(exitCode).toBe(1);
});
test.each([
{
name: "bun list executes pm ls",
cmd: ["list"],
packageName: "test-list",
dependencies: { bar: "latest" },
expectedOutput: (dir: string) => `${dir} node_modules (1)\n└── bar@0.0.2\n`,
checkReservationMessage: true,
},
{
name: "bun pm list works as alias for bun pm ls",
cmd: ["pm", "list"],
packageName: "test-pm-list",
dependencies: { bar: "latest" },
expectedOutput: (dir: string) => `${dir} node_modules (1)\n└── bar@0.0.2\n`,
checkReservationMessage: false,
},
{
name: "bun pm ls still works",
cmd: ["pm", "ls"],
packageName: "test-pm-ls",
dependencies: { bar: "latest" },
expectedOutput: (dir: string) => `${dir} node_modules (1)\n└── bar@0.0.2\n`,
checkReservationMessage: false,
},
])("$name", async ({ cmd, packageName, dependencies, expectedOutput, checkReservationMessage }) => {
const urls: string[] = [];
setHandler(dummyRegistry(urls));
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({
name: packageName,
version: "1.0.0",
dependencies,
}),
);
// Install dependencies first
{
const { stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const err = await stderr.text();
expect(err).not.toContain("error:");
expect(err).toContain("Saved lockfile");
expect(await exited).toBe(0);
}
// Test the command
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), ...cmd],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const [stderrText, stdoutText, exitCode] = await Promise.all([
new Response(stderr).text(),
new Response(stdout).text(),
exited,
]);
expect(stderrText).toBe("");
if (checkReservationMessage) {
expect(stdoutText).not.toContain("reserved for future use");
}
expect(stdoutText).toBe(expectedOutput(package_dir));
expect(exitCode).toBe(0);
});
test("bun list --all shows full dependency tree", async () => {
const urls: string[] = [];
setHandler(dummyRegistry(urls));
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({
name: "test-list-all",
version: "1.0.0",
dependencies: {
moo: "./moo",
},
}),
);
await mkdir(join(package_dir, "moo"));
await writeFile(
join(package_dir, "moo", "package.json"),
JSON.stringify({
name: "moo",
version: "0.1.0",
dependencies: {
bar: "latest",
},
}),
);
// Install dependencies first
{
const { stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const err = await stderr.text();
expect(err).not.toContain("error:");
expect(err).toContain("Saved lockfile");
expect(await exited).toBe(0);
}
// Test "bun list --all"
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "list", "--all"],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const [stderrText, stdoutText, exitCode] = await Promise.all([
new Response(stderr).text(),
new Response(stdout).text(),
exited,
]);
expect(stderrText).toBe("");
expect(stdoutText).toBe(`${package_dir} node_modules
├── bar@0.0.2
└── moo@moo
`);
expect(exitCode).toBe(0);
});