Files
bun-src/test/cli/install/bun-dedupe.test.ts
2026-08-27 21:09:14 +00:00

2234 lines
94 KiB
TypeScript

import { file, write } from "bun";
import { afterAll, beforeAll, expect, test } from "bun:test";
import { copyFile, exists, mkdir, realpath, rm } from "fs/promises";
import { VerdaccioRegistry, bunEnv, bunExe, normalizeBunSnapshot, readdirSorted, runBunInstall } from "harness";
import { dirname, join } from "path";
const registry = new VerdaccioRegistry();
beforeAll(async () => {
await registry.start();
});
afterAll(() => {
registry.stop();
});
// CI exports BUN_INSTALL_CACHE_DIR, which overrides the harness bunfig's per-test `cache`; concurrent cases sharing one cache race on Windows.
const installEnv = (dir: string) => ({ ...bunEnv, BUN_INSTALL_CACHE_DIR: join(dir, ".bun-cache") });
async function run(dir: string, ...cmd: string[]) {
await using proc = Bun.spawn({
cmd: [bunExe(), ...cmd],
env: installEnv(dir),
cwd: dir,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
return { stdout, stderr, exitCode };
}
const dedupe = (dir: string, ...args: string[]) => run(dir, "dedupe", ...args);
function lock(dir: string) {
return file(join(dir, "bun.lock")).text();
}
const HEADER = "bun dedupe <version> (<revision>)";
const lines = (stdout: string) => normalizeBunSnapshot(stdout).split("\n");
function nodeModulesVersion(packageDir: string, ...segments: string[]) {
return file(join(packageDir, "node_modules", ...segments, "package.json"))
.json()
.then(pkg => pkg.version);
}
// Every summary counts the lockfile's package entries (root included); `packages` keys are node_modules paths, so one entry can sit under several keys.
function lockPackageCount(lockfile: string) {
const { packages = {} } = Bun.JSONC.parse(lockfile) as { packages?: Record<string, [string, ...unknown[]]> };
return 1 + new Set(Object.values(packages).map(([resolution]) => resolution)).size;
}
const packagesWord = (n: number) => `${n} package${n === 1 ? "" : "s"}`;
const versionsWord = (n: number) => `${n} duplicate version${n === 1 ? "" : "s"}`;
const noDuplicates = (checked: number) =>
`🎉 No duplicates — checked ${packagesWord(checked)}, every one already resolves to a single version`;
const wouldRemove = (removed: number, checked: number) =>
`${versionsWord(removed)} can be removed (checked ${packagesWord(checked)})`;
const HINT = " bun dedupe";
const row = (label: string) => `~ ${label}`;
// normalizeBunSnapshot strips durations, so the summary line is checked for one on the raw output.
function expectTimed(stdout: string, summary: RegExp) {
const line = stdout.split("\n").find(line => summary.test(line));
expect(line).toMatch(/ \[\d+\.\d\d(ms|s)\]\r?$/);
expect(stdout.match(/ \[\d+\.\d\d(ms|s)\]\r?$/gm)).toHaveLength(1);
}
const WOULD_REMOVE = /^\d+ duplicate versions? can be removed /;
const REMOVED = /^\d+ duplicate versions? removed/;
const NO_DUPLICATES = /^🎉 /;
function removedSummary(removed: number, checked?: number) {
return new RegExp(
`^${versionsWord(removed)} removed(, [1-9]\\d* packages? installed)? \\(checked ${
checked === undefined ? "[1-9]\\d* packages?" : packagesWord(checked)
}\\)$`,
);
}
type Removed = { kept?: string[]; checked?: number };
// The rows, kept lines, a blank line and the summary end stdout; whatever the install printed sits between the header and the first row.
function expectRemoved(stdout: string, rows: string | string[], { kept = [], checked }: Removed = {}) {
rows = [rows].flat();
const out = lines(stdout);
expect(out[0]).toBe(HEADER);
const blockStart = out.length - rows.length - kept.length - 2;
expect(out.slice(blockStart, -1)).toStrictEqual([...rows.map(row), ...kept, ""]);
expect(out.at(-1)).toMatch(removedSummary(rows.length, checked));
expectTimed(stdout, REMOVED);
expect(
out
.slice(1, blockStart)
.filter(
line =>
line.startsWith("~ ") ||
line.startsWith("+ ") ||
line.includes("duplicate") ||
line.includes("kept ") ||
line.includes("Checked ") ||
line.includes("installed") ||
line.includes("no changes"),
),
).toStrictEqual([]);
}
type Result = Awaited<ReturnType<typeof run>>;
function expectWouldRemove({ stdout, stderr }: Result, rows: string | string[], checked: number, kept: string[] = []) {
rows = [rows].flat();
expect(lines(stdout)).toStrictEqual([HEADER, ...rows.map(row), ...kept, "", wouldRemove(rows.length, checked), HINT]);
expectTimed(stdout, WOULD_REMOVE);
expect(stderr).not.toContain("Saved lockfile");
expect(stderr).not.toContain("error:");
}
// One line, one duration, nothing saved: the same on `--check` and on a plain `bun dedupe`.
function expectNoDuplicates({ stdout, stderr, exitCode }: Result, checked?: number, kept: string[] = []) {
expect(lines(stdout)).toStrictEqual([
HEADER,
...kept,
...(kept.length ? [""] : []),
checked === undefined
? expect.stringMatching(/^🎉 No duplicates — checked [1-9]\d* packages?, /)
: noDuplicates(checked),
]);
expectTimed(stdout, NO_DUPLICATES);
expect(stderr).not.toContain("error:");
expect(stderr).not.toContain("Saved lockfile");
expect(exitCode).toBe(0);
}
async function expectAlreadyDeduplicated(dir: string, checked?: number, kept: string[] = []) {
expectNoDuplicates(await dedupe(dir, "--check"), checked, kept);
expectNoDuplicates(await dedupe(dir), checked, kept);
}
async function expectRefused(dir: string, ...args: string[]) {
const { stdout, stderr, exitCode } = await dedupe(dir, ...args);
expect(lines(stderr)).toStrictEqual([
"error: bun.lock does not match package.json, nothing to dedupe",
"note: run 'bun install' first",
]);
expect(lines(stdout)).toStrictEqual([HEADER]);
expect(exitCode).toBe(1);
}
// bun.lock writes one package entry per line; edits the entry whose line starts with `entryPrefix`.
function editLockEntry(lockfile: string, entryPrefix: string, from: string, to: string) {
const lines = lockfile.split("\n");
const i = lines.findIndex(line => line.trimStart().startsWith(entryPrefix));
expect(i).not.toBe(-1);
expect(lines[i]).toContain(from);
lines[i] = lines[i].replace(from, to);
return lines.join("\n");
}
// Same trick as 'never upgrades past the locked version': widen the range in both files, keeping the locked resolution.
async function widen(packageDir: string, packageJsonPath: string, name: string, range: string) {
const [pkg, lockfile] = await Promise.all([file(packageJsonPath).json(), lock(packageDir)]);
const from = `"${name}": "${pkg.dependencies[name]}"`;
expect(lockfile.split(from)).toHaveLength(2);
pkg.dependencies[name] = range;
await Promise.all([
write(packageJsonPath, JSON.stringify(pkg)),
write(join(packageDir, "bun.lock"), lockfile.replace(from, `"${name}": "${range}"`)),
]);
}
const noDepsPatch = `diff --git a/patched.txt b/patched.txt
new file mode 100644
index 0000000000000000000000000000000000000000..3b18e512dba79e4c8300dd08aeb37f8e728b8dad
--- /dev/null
+++ b/patched.txt
@@ -0,0 +1 @@
+hello world
`;
// A still-satisfied range edge is never re-resolved by a later install, so adding an exact pin afterwards leaves a duplicate.
async function installTwice(packageDir: string, packageJson: string, first: object, second: object) {
await write(packageJson, JSON.stringify(first));
await runBunInstall(installEnv(packageDir), packageDir);
await write(packageJson, JSON.stringify(second));
await runBunInstall(installEnv(packageDir), packageDir);
return lock(packageDir);
}
// [email protected] depends on no-deps@^1.0.0 (locked 1.1.0); root then pins [email protected].
async function setupRangeDuplicate(packageDir: string, packageJson: string) {
const lockfile = await installTwice(
packageDir,
packageJson,
{ name: "foo", dependencies: { "one-range-dep": "1.0.0" } },
{ name: "foo", dependencies: { "one-range-dep": "1.0.0", "no-deps": "1.0.0" } },
);
expect(lockfile).toContain('"[email protected]"');
expect(lockfile).toContain('"[email protected]"');
expect(lockfile).toContain('"one-range-dep/no-deps"');
return lockfile;
}
test.concurrent("collapses a range onto the exact version that satisfies every edge", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
await setupRangeDuplicate(packageDir, packageJson);
const pkgJsonBefore = await file(packageJson).text();
const nested = join(packageDir, "node_modules", "one-range-dep", "node_modules", "no-deps");
expect(await nodeModulesVersion(packageDir, "one-range-dep", "node_modules", "no-deps")).toBe("1.1.0");
const { stdout, stderr, exitCode } = await dedupe(packageDir);
expect(lines(stdout)).toStrictEqual([
HEADER,
"~ no-deps 1.1.0 -> 1.0.0",
"",
"1 duplicate version removed (checked 4 packages)",
]);
expectRemoved(stdout, "no-deps 1.1.0 -> 1.0.0", { checked: 4 });
expect(lines(stderr)).toStrictEqual(["Saved lockfile"]);
expect(exitCode).toBe(0);
expect(await exists(nested)).toBeFalse();
expect(await nodeModulesVersion(packageDir, "no-deps")).toBe("1.0.0");
const lockfile = await lock(packageDir);
expect(lockfile).toContain('"[email protected]"');
expect(lockfile).not.toContain('"[email protected]"');
expect(lockfile).not.toContain('"one-range-dep/no-deps"');
expect(await file(packageJson).text()).toBe(pkgJsonBefore);
await runBunInstall(installEnv(packageDir), packageDir, { frozenLockfile: true });
});
// pnpm/pnpm#6550: root's range moves up onto the version its dependents already use.
test.concurrent("prefers the highest version when several satisfy every edge", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
await setupRangeDuplicate(packageDir, packageJson);
await write(
packageJson,
JSON.stringify({ name: "foo", dependencies: { "one-range-dep": "1.0.0", "no-deps": "^1.0.0" } }),
);
await runBunInstall(installEnv(packageDir), packageDir);
let lockfile = await lock(packageDir);
expect(lockfile).toContain('"[email protected]"');
expect(lockfile).toContain('"[email protected]"');
const { stdout, stderr, exitCode } = await dedupe(packageDir);
expectRemoved(stdout, "no-deps 1.0.0 -> 1.1.0", { checked: 4 });
expect(stderr).not.toContain("error:");
expect(exitCode).toBe(0);
lockfile = await lock(packageDir);
expect(lockfile).not.toContain('"[email protected]"');
expect(lockfile).toContain('"[email protected]"');
expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toStrictEqual({
name: "no-deps",
version: "1.1.0",
});
await runBunInstall(installEnv(packageDir), packageDir, { frozenLockfile: true });
});
// pnpm/pnpm#13503: `--check` must not touch node_modules either.
test.concurrent("--check reports and exits 1 without writing", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const lockBefore = await setupRangeDuplicate(packageDir, packageJson);
const pkgJsonBefore = await file(packageJson).text();
const nodeModulesBefore = await readdirSorted(join(packageDir, "node_modules"));
const nestedPkgJson = join(packageDir, "node_modules", "one-range-dep", "node_modules", "no-deps", "package.json");
expect(await file(nestedPkgJson).json()).toStrictEqual({ name: "no-deps", version: "1.1.0" });
expect(lockPackageCount(lockBefore)).toBe(4);
const check = await dedupe(packageDir, "--check");
expect(lines(check.stdout)).toMatchInlineSnapshot(`
[
"bun dedupe <version> (<revision>)",
"~ no-deps 1.1.0 -> 1.0.0",
"",
"1 duplicate version can be removed (checked 4 packages)",
" bun dedupe",
]
`);
expectWouldRemove(check, "no-deps 1.1.0 -> 1.0.0", 4);
expect(check.stderr).toBe("");
expect(check.exitCode).toBe(1);
expect(await lock(packageDir)).toBe(lockBefore);
expect(await file(packageJson).text()).toBe(pkgJsonBefore);
expect(await readdirSorted(join(packageDir, "node_modules"))).toStrictEqual(nodeModulesBefore);
expect(await file(nestedPkgJson).json()).toStrictEqual({ name: "no-deps", version: "1.1.0" });
});
// `--dry-run` shows the plan and succeeds like `bun prune --dry-run`; only `--check` is the CI gate.
test.concurrent("--dry-run prints what --check prints and exits 0", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const lockBefore = await setupRangeDuplicate(packageDir, packageJson);
const dryRun = await dedupe(packageDir, "--dry-run");
expectWouldRemove(dryRun, "no-deps 1.1.0 -> 1.0.0", 4);
expect(dryRun.stderr).toBe("");
expect(dryRun.exitCode).toBe(0);
expect(await lock(packageDir)).toBe(lockBefore);
const check = await dedupe(packageDir, "--check");
expect(normalizeBunSnapshot(check.stdout)).toBe(normalizeBunSnapshot(dryRun.stdout));
expect(check.exitCode).toBe(1);
const both = await dedupe(packageDir, "--dry-run", "--check");
expect(normalizeBunSnapshot(both.stdout)).toBe(normalizeBunSnapshot(dryRun.stdout));
expect(both.exitCode).toBe(1);
expect(await lock(packageDir)).toBe(lockBefore);
expect(await nodeModulesVersion(packageDir, "one-range-dep", "node_modules", "no-deps")).toBe("1.1.0");
});
test.concurrent("--no-summary keeps the rows and the hint but drops the count line", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const lockBefore = await setupRangeDuplicate(packageDir, packageJson);
const check = await dedupe(packageDir, "--check", "--no-summary");
expect(lines(check.stdout).filter(Boolean)).toStrictEqual(["~ no-deps 1.1.0 -> 1.0.0", HINT]);
expect(check.stdout).not.toMatch(/\[\d+\.\d\d(ms|s)\]/);
expect(check.stderr).toBe("");
expect(check.exitCode).toBe(1);
expect(await lock(packageDir)).toBe(lockBefore);
const dryRun = await dedupe(packageDir, "--dry-run", "--no-summary");
expect(dryRun.stdout).toBe(check.stdout);
expect(dryRun.exitCode).toBe(0);
expect(await lock(packageDir)).toBe(lockBefore);
});
test.concurrent("already deduplicated", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
await setupRangeDuplicate(packageDir, packageJson);
const first = await dedupe(packageDir);
expect(first.stderr).not.toContain("error:");
expect(first.exitCode).toBe(0);
const lockAfterFirst = await lock(packageDir);
expect(lockAfterFirst).not.toContain('"[email protected]"');
expect(lockPackageCount(lockAfterFirst)).toBe(3);
const second = await dedupe(packageDir);
expect(lines(second.stdout)).toMatchInlineSnapshot(`
[
"bun dedupe <version> (<revision>)",
"🎉 No duplicates — checked 3 packages, every one already resolves to a single version",
]
`);
expect(second.stderr).toBe("");
expectNoDuplicates(second, 3);
expect(await lock(packageDir)).toBe(lockAfterFirst);
for (const flag of ["--check", "--dry-run", "--frozen-lockfile", "--production", "--no-save"]) {
const rerun = await dedupe(packageDir, flag);
expect(normalizeBunSnapshot(rerun.stdout)).toBe(normalizeBunSnapshot(second.stdout));
expect(rerun.stderr).toBe("");
expect(rerun.exitCode).toBe(0);
expect(await lock(packageDir)).toBe(lockAfterFirst);
}
});
test.concurrent.each(["hoisted", "isolated"] as const)("a no-op prints one summary line (%s linker)", async linker => {
const { packageDir, packageJson } = await registry.createTestDir({ bunfigOpts: { linker } });
await write(packageJson, JSON.stringify({ name: "foo", dependencies: { "one-range-dep": "1.0.0" } }));
await runBunInstall(installEnv(packageDir), packageDir);
const lockBefore = await lock(packageDir);
expect(lockPackageCount(lockBefore)).toBe(3);
for (const args of [[], ["--check"], ["--lockfile-only"]]) {
const result = await dedupe(packageDir, ...args, "--linker", linker);
expectNoDuplicates(result, 3);
expect(result.stderr).toBe("");
expect(await lock(packageDir)).toBe(lockBefore);
}
});
test.concurrent("keeps versions that no single version can replace", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
await write(
packageJson,
JSON.stringify({ name: "foo", dependencies: { "one-dep": "1.0.0", "one-fixed-dep": "1.0.0" } }),
);
await runBunInstall(installEnv(packageDir), packageDir);
const lockBefore = await lock(packageDir);
expect(lockBefore).toContain('"[email protected]"');
expect(lockBefore).toContain('"[email protected]"');
await expectAlreadyDeduplicated(packageDir, 5);
const lockAfter = await lock(packageDir);
expect(lockAfter).toContain('"[email protected]"');
expect(lockAfter).toContain('"[email protected]"');
expect(lockAfter).toBe(lockBefore);
});
test.concurrent("honours catalog ranges", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const root = (catalogRange: string, dependencies?: Record<string, string>) =>
JSON.stringify({ name: "root", workspaces: ["packages/*"], catalog: { "no-deps": catalogRange }, dependencies });
await Promise.all([
write(packageJson, root("1.0.0")),
write(
join(packageDir, "packages", "a", "package.json"),
JSON.stringify({ name: "a", dependencies: { "no-deps": "catalog:" } }),
),
]);
await runBunInstall(installEnv(packageDir), packageDir);
let lockfile = await lock(packageDir);
expect(lockfile).toContain('"[email protected]"');
await write(packageJson, root("^1.0.0"));
await runBunInstall(installEnv(packageDir), packageDir);
lockfile = await lock(packageDir);
expect(lockfile).toContain('"[email protected]"');
expect(lockfile).not.toContain('"[email protected]"');
await write(packageJson, root("^1.0.0", { "no-deps": "1.1.0" }));
await runBunInstall(installEnv(packageDir), packageDir);
lockfile = await lock(packageDir);
expect(lockfile).toContain('"[email protected]"');
expect(lockfile).toContain('"[email protected]"');
const { stdout, stderr, exitCode } = await dedupe(packageDir);
expectRemoved(stdout, "no-deps 1.0.0 -> 1.1.0", { checked: 4 });
expect(stderr).not.toContain("error:");
expect(exitCode).toBe(0);
lockfile = await lock(packageDir);
expect(lockfile).not.toContain('"[email protected]"');
expect(lockfile).toContain('"[email protected]"');
await runBunInstall(installEnv(packageDir), packageDir, { frozenLockfile: true });
});
test.concurrent("override range wins over the edge's own range", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const lockBefore = await setupRangeDuplicate(packageDir, packageJson);
expect(lockBefore).toContain('\n "packages": {');
const withOverride = lockBefore.replace(
'\n "packages": {',
'\n "overrides": {\n "no-deps": "1.1.0",\n },\n "packages": {',
);
await Promise.all([
write(join(packageDir, "bun.lock"), withOverride),
write(
packageJson,
JSON.stringify({
name: "foo",
dependencies: { "one-range-dep": "1.0.0", "no-deps": "1.0.0" },
overrides: { "no-deps": "1.1.0" },
}),
),
]);
const check = await dedupe(packageDir, "--check");
expectWouldRemove(check, "no-deps 1.0.0 -> 1.1.0", 4);
expect(check.exitCode).toBe(1);
expect(await lock(packageDir)).toBe(withOverride);
});
test.concurrent("errors without a lockfile", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
await write(packageJson, JSON.stringify({ name: "foo", dependencies: { "no-deps": "1.0.0" } }));
const apply = await dedupe(packageDir);
expect(apply.stderr).toContain("error: missing lockfile, nothing to dedupe");
expect(apply.exitCode).toBe(1);
const check = await dedupe(packageDir, "--check");
expect(check.stderr).toContain("error: missing lockfile, nothing to dedupe");
expect(check.exitCode).toBe(1);
expect(await exists(join(packageDir, "bun.lock"))).toBeFalse();
});
test.concurrent("rejects positional arguments", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
await write(packageJson, JSON.stringify({ name: "foo", dependencies: { "no-deps": "1.0.0" } }));
await runBunInstall(installEnv(packageDir), packageDir);
const lockBefore = await lock(packageDir);
const { stderr, exitCode } = await dedupe(packageDir, "no-deps");
expect(stderr).toContain("error: bun dedupe does not take arguments");
expect(exitCode).toBe(1);
expect(await lock(packageDir)).toBe(lockBefore);
});
test.concurrent("--help", async () => {
const { packageDir } = await registry.createTestDir();
const { stdout, stderr, exitCode } = await dedupe(packageDir, "--help");
expect(stdout).toContain("Usage: bun dedupe [flags]");
const out = lines(stdout);
const flagsStart = out.indexOf("Flags:") + 1;
expect(flagsStart).toBeGreaterThan(0);
const flagLines = out.slice(flagsStart, out.indexOf("", flagsStart));
expect(flagLines.map(line => line.match(/--[\w-]+/)![0])).toStrictEqual([
"--check",
"--dry-run",
"--lockfile-only",
"--frozen-lockfile",
"--linker",
"--silent",
"--cwd",
"--help",
]);
expect(flagLines.find(line => line.includes("--dry-run"))).toContain("without changing anything");
expect(flagLines.find(line => line.includes("--help"))).toStartWith(" -h, --help");
expect(stderr).toBe("");
expect(exitCode).toBe(0);
});
test.concurrent("works with the isolated linker", async () => {
const { packageDir, packageJson } = await registry.createTestDir({ bunfigOpts: { linker: "isolated" } });
const install = async (...args: string[]) => {
const result = await run(packageDir, "install", ...args, "--linker", "isolated");
expect(result.stderr).not.toContain("error:");
expect(result.exitCode).toBe(0);
return result;
};
await write(packageJson, JSON.stringify({ name: "foo", dependencies: { "one-range-dep": "1.0.0" } }));
await install();
await write(
packageJson,
JSON.stringify({ name: "foo", dependencies: { "one-range-dep": "1.0.0", "no-deps": "1.0.0" } }),
);
await install();
let lockfile = await lock(packageDir);
expect(lockfile).toContain('"[email protected]"');
expect(lockfile).toContain('"[email protected]"');
// Store entry names may carry hash suffixes, so reach one-range-dep's store node_modules through the top-level link.
const nestedNoDeps = async () => {
const storeEntry = await realpath(join(packageDir, "node_modules", "one-range-dep"));
return file(join(dirname(storeEntry), "no-deps", "package.json")).json();
};
expect(await nestedNoDeps()).toStrictEqual({ name: "no-deps", version: "1.1.0" });
const check = await dedupe(packageDir, "--check", "--linker", "isolated");
expectWouldRemove(check, "no-deps 1.1.0 -> 1.0.0", 4);
expect(check.exitCode).toBe(1);
const { stdout, stderr, exitCode } = await dedupe(packageDir, "--linker", "isolated");
expectRemoved(stdout, "no-deps 1.1.0 -> 1.0.0", { checked: 4 });
expect(stderr).toContain("Saved lockfile");
expect(stderr).not.toContain("error:");
expect(exitCode).toBe(0);
lockfile = await lock(packageDir);
expect(lockfile).toContain('"[email protected]"');
expect(lockfile).not.toContain('"[email protected]"');
await install("--frozen-lockfile");
expect(await nestedNoDeps()).toStrictEqual({ name: "no-deps", version: "1.0.0" });
});
// Loading re-hoists an optional peer onto a satisfying no-deps placed before it, so every other holder must sort after one-optional-peer-dep (hence the alias name).
test.concurrent("duplicate held only by an optional peer edge is deduplicated", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const lockfile = await installTwice(
packageDir,
packageJson,
{ name: "foo", dependencies: { "one-optional-peer-dep": "1.0.2", "one-range-dep": "1.0.0" } },
{
name: "foo",
dependencies: {
"one-optional-peer-dep": "1.0.2",
"one-range-dep": "1.0.0",
"z-fixed-dep": "npm:[email protected]",
},
},
);
expect(lockfile).toContain('"no-deps": ["[email protected]"');
expect(lockfile).toContain('"z-fixed-dep/no-deps": ["[email protected]"');
// Swap placements: root's no-deps becomes 1.0.0 (still satisfying one-range-dep and z-fixed-dep); only the peer edge resolves to 1.1.0.
const heldByPeer = lockfile
.replace('"no-deps": ["[email protected]"', '"one-optional-peer-dep/no-deps": ["[email protected]"')
.replace('"z-fixed-dep/no-deps": ["[email protected]"', '"no-deps": ["[email protected]"');
await write(join(packageDir, "bun.lock"), heldByPeer);
const checked = lockPackageCount(heldByPeer);
const check = await dedupe(packageDir, "--check");
expectWouldRemove(check, "no-deps 1.1.0 -> 1.0.0", checked);
expect(check.exitCode).toBe(1);
expect(await lock(packageDir)).toBe(heldByPeer);
const { stdout, stderr, exitCode } = await dedupe(packageDir);
expectRemoved(stdout, "no-deps 1.1.0 -> 1.0.0", { checked });
expect(stderr).not.toContain("error:");
expect(exitCode).toBe(0);
const after = await lock(packageDir);
expect(after).toContain('"[email protected]"');
expect(after).not.toContain('"[email protected]"');
expect(lockPackageCount(after)).toBe(checked - 1);
await expectAlreadyDeduplicated(packageDir, checked - 1);
await runBunInstall(installEnv(packageDir), packageDir, { frozenLockfile: true });
});
const REFUSALS: [string, string][] = [
["--frozen-lockfile", "--frozen-lockfile was passed"],
["--production", "--production implies --frozen-lockfile"],
["--no-save", "--no-save was passed"],
];
function refusalStderr(removed: number, reason: string) {
return [
`error: ${versionsWord(removed)} can be removed, but ${reason}`,
`note: run 'bun dedupe' to remove ${removed === 1 ? "it" : "them"}, or 'bun dedupe --check' in CI`,
];
}
// The plan is reported exactly like --check; the error and its remedy stay together on stderr so `2>/dev/null` leaves no orphan hint.
function expectRefusedToWrite(
{ stdout, stderr, exitCode }: Result,
rows: string | string[],
checked: number,
reason: string,
kept: string[] = [],
) {
rows = [rows].flat();
expect(lines(stdout)).toStrictEqual([HEADER, ...rows.map(row), ...kept, "", wouldRemove(rows.length, checked)]);
expectTimed(stdout, WOULD_REMOVE);
expect(lines(stderr)).toStrictEqual(refusalStderr(rows.length, reason));
expect(exitCode).toBe(1);
}
test.concurrent.each(REFUSALS)("%s with duplicates errors instead of claiming removal", async (flag, reason) => {
const { packageDir, packageJson } = await registry.createTestDir();
const lockBefore = await setupRangeDuplicate(packageDir, packageJson);
const result = await dedupe(packageDir, flag);
expectRefusedToWrite(result, "no-deps 1.1.0 -> 1.0.0", 4, reason);
expect(await lock(packageDir)).toBe(lockBefore);
expect(await nodeModulesVersion(packageDir, "one-range-dep", "node_modules", "no-deps")).toBe("1.1.0");
});
test.concurrent("--lockfile-only still refuses under --frozen-lockfile", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const lockBefore = await setupRangeDuplicate(packageDir, packageJson);
const lockfileOnly = await dedupe(packageDir, "--frozen-lockfile", "--lockfile-only");
expectRefusedToWrite(lockfileOnly, "no-deps 1.1.0 -> 1.0.0", 4, "--frozen-lockfile was passed");
expect(await lock(packageDir)).toBe(lockBefore);
});
test.concurrent.each(REFUSALS)("%s succeeds when already deduplicated", async flag => {
const { packageDir, packageJson } = await registry.createTestDir();
await write(packageJson, JSON.stringify({ name: "foo", dependencies: { "one-range-dep": "1.0.0" } }));
await runBunInstall(installEnv(packageDir), packageDir);
const lockBefore = await lock(packageDir);
const result = await dedupe(packageDir, flag);
expectNoDuplicates(result, 3);
expect(result.stderr).toBe("");
expect(await lock(packageDir)).toBe(lockBefore);
});
// Modelled on pnpm's workspace-with-lockfile-dupes fixture: root's ">=1.0.0" edge collapses onto the aliased 1.0.0, orphaning [email protected] -> [email protected].
test.concurrent("cascading removal lists every unreachable duplicate in name order", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const lockfile = await installTwice(
packageDir,
packageJson,
{ name: "foo", dependencies: { "one-fixed-dep": ">=1.0.0" } },
{ name: "foo", dependencies: { "one-fixed-dep": ">=1.0.0", "ofd": "npm:[email protected]" } },
);
for (const label of ['"[email protected]"', '"[email protected]"', '"[email protected]"', '"[email protected]"']) {
expect(lockfile).toContain(label);
}
const rows = ["no-deps 2.0.0", "one-fixed-dep 2.0.0 -> 1.0.0"];
const checked = lockPackageCount(lockfile);
expect(checked).toBe(5);
const check = await dedupe(packageDir, "--check");
expectWouldRemove(check, rows, checked);
expect(check.exitCode).toBe(1);
const { stdout, stderr, exitCode } = await dedupe(packageDir);
expectRemoved(stdout, rows, { checked });
expect(stderr).not.toContain("error:");
expect(exitCode).toBe(0);
const after = await lock(packageDir);
expect(after).toContain('"[email protected]"');
expect(after).toContain('"[email protected]"');
expect(after).not.toContain('"[email protected]"');
expect(after).not.toContain('"[email protected]"');
expect(await nodeModulesVersion(packageDir, "one-fixed-dep")).toBe("1.0.0");
expect(await nodeModulesVersion(packageDir, "ofd")).toBe("1.0.0");
await runBunInstall(installEnv(packageDir), packageDir, { frozenLockfile: true });
});
test.concurrent("multiple names removed in one run are sorted, scoped names first", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const lockfile = await installTwice(
packageDir,
packageJson,
{ name: "foo", dependencies: { "two-range-deps": "1.0.0" } },
{ name: "foo", dependencies: { "two-range-deps": "1.0.0", "no-deps": "1.0.0", "@types/is-number": "1.0.0" } },
);
for (const label of ['"[email protected]"', '"[email protected]"', '"@types/[email protected]"', '"@types/[email protected]"']) {
expect(lockfile).toContain(label);
}
const rows = ["@types/is-number 2.0.0 -> 1.0.0", "no-deps 1.1.0 -> 1.0.0"];
const checked = lockPackageCount(lockfile);
expect(checked).toBe(6);
const check = await dedupe(packageDir, "--check");
expectWouldRemove(check, rows, checked);
expect(check.exitCode).toBe(1);
const frozen = await dedupe(packageDir, "--frozen-lockfile");
expectRefusedToWrite(frozen, rows, checked, "--frozen-lockfile was passed");
expect(await lock(packageDir)).toBe(lockfile);
const { stdout, exitCode } = await dedupe(packageDir);
expectRemoved(stdout, rows, { checked });
expect(exitCode).toBe(0);
const after = await lock(packageDir);
expect(after).not.toContain('"@types/[email protected]"');
expect(after).not.toContain('"[email protected]"');
await runBunInstall(installEnv(packageDir), packageDir, { frozenLockfile: true });
});
test.concurrent("workspace package edge is re-pointed when run from the workspace directory", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const workspaceDir = join(packageDir, "packages", "a");
await write(
join(workspaceDir, "package.json"),
JSON.stringify({ name: "a", dependencies: { "one-range-dep": "1.0.0" } }),
);
const lockfile = await installTwice(
packageDir,
packageJson,
{ name: "root", workspaces: ["packages/*"] },
{ name: "root", workspaces: ["packages/*"], dependencies: { "no-deps": "1.0.0" } },
);
expect(lockfile).toContain('"[email protected]"');
expect(lockfile).toContain('"[email protected]"');
const { stdout, stderr, exitCode } = await dedupe(workspaceDir);
expectRemoved(stdout, "no-deps 1.1.0 -> 1.0.0");
expect(stderr).not.toContain("error:");
expect(exitCode).toBe(0);
const after = await lock(packageDir);
expect(after).not.toContain('"[email protected]"');
expect(await exists(join(workspaceDir, "bun.lock"))).toBeFalse();
await runBunInstall(installEnv(packageDir), packageDir, { frozenLockfile: true });
});
test.concurrent("corrupt bun.lock fails without rewriting it", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
await write(packageJson, JSON.stringify({ name: "foo", dependencies: { "no-deps": "1.0.0" } }));
await runBunInstall(installEnv(packageDir), packageDir);
const corrupt = "{ not valid";
await write(join(packageDir, "bun.lock"), corrupt);
for (const args of [[], ["--check"]]) {
const { stderr, exitCode } = await dedupe(packageDir, ...args);
expect(stderr).toContain("failed to parse lockfile");
expect(stderr).not.toContain("Ignoring lockfile");
expect(stderr).not.toContain("Saved lockfile");
expect(exitCode).not.toBe(0);
expect(await lock(packageDir)).toBe(corrupt);
}
});
test.concurrent("--check never creates node_modules", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const lockBefore = await setupRangeDuplicate(packageDir, packageJson);
await rm(join(packageDir, "node_modules"), { recursive: true });
for (const flag of ["--check", "--dry-run"]) {
const check = await dedupe(packageDir, flag);
expectWouldRemove(check, "no-deps 1.1.0 -> 1.0.0", 4);
expect(check.exitCode).toBe(flag === "--check" ? 1 : 0);
expect(await exists(join(packageDir, "node_modules"))).toBeFalse();
expect(await lock(packageDir)).toBe(lockBefore);
}
await runBunInstall(installEnv(packageDir), packageDir, { savesLockfile: false });
const apply = await dedupe(packageDir);
expectRemoved(apply.stdout, "no-deps 1.1.0 -> 1.0.0", { checked: 4 });
expect(apply.exitCode).toBe(0);
const lockDeduped = await lock(packageDir);
await rm(join(packageDir, "node_modules"), { recursive: true });
// A no-op exits before the install step, so a plain `bun dedupe` creates nothing either.
await expectAlreadyDeduplicated(packageDir, 3);
expect(await exists(join(packageDir, "node_modules"))).toBeFalse();
expect(await lock(packageDir)).toBe(lockDeduped);
});
test.concurrent("never upgrades past the locked version", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
await write(packageJson, JSON.stringify({ name: "foo", dependencies: { "no-deps": "1.0.0" } }));
await runBunInstall(installEnv(packageDir), packageDir);
const pinned = await lock(packageDir);
expect(pinned).toContain('"no-deps": "1.0.0"');
const widened = pinned.replace('"no-deps": "1.0.0"', '"no-deps": "^1.0.0"');
await Promise.all([
write(join(packageDir, "bun.lock"), widened),
write(packageJson, JSON.stringify({ name: "foo", dependencies: { "no-deps": "^1.0.0" } })),
]);
await expectAlreadyDeduplicated(packageDir, 2);
const after = await lock(packageDir);
expect(after).toContain('"[email protected]"');
expect(after).not.toContain('"[email protected]"');
expect(await nodeModulesVersion(packageDir, "no-deps")).toBe("1.0.0");
});
test.concurrent.each([
["skipped with --ignore-scripts", ["--ignore-scripts"], false],
["run by default", [], true],
])("root lifecycle scripts are %s", async (_, flags, runs) => {
const { packageDir, packageJson } = await registry.createTestDir();
await setupRangeDuplicate(packageDir, packageJson);
// Added after setup so only the dedupe run can create the marker.
const pkg = await file(packageJson).json();
await write(packageJson, JSON.stringify({ ...pkg, scripts: { postinstall: "echo ran > postinstall.txt" } }));
const marker = join(packageDir, "postinstall.txt");
const { stdout, stderr, exitCode } = await dedupe(packageDir, ...flags);
expectRemoved(stdout, "no-deps 1.1.0 -> 1.0.0");
expect(stderr).not.toContain("error:");
expect(exitCode).toBe(0);
expect(await lock(packageDir)).not.toContain('"[email protected]"');
expect(await exists(marker)).toBe(runs);
});
test.concurrent("the report is printed as one block after root lifecycle script output", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
await setupRangeDuplicate(packageDir, packageJson);
const pkg = await file(packageJson).json();
await write(packageJson, JSON.stringify({ ...pkg, scripts: { postinstall: "echo postinstall ran" } }));
const { stdout, stderr, exitCode } = await dedupe(packageDir);
const out = lines(stdout);
const scriptOutput = out.indexOf("postinstall ran");
const firstRow = out.findIndex(line => line.startsWith("~ "));
expect(scriptOutput).toBeGreaterThan(0);
expect(firstRow).toBeGreaterThan(scriptOutput);
expect(out.slice(firstRow)).toStrictEqual([
"~ no-deps 1.1.0 -> 1.0.0",
"",
"1 duplicate version removed (checked 4 packages)",
]);
expectRemoved(stdout, "no-deps 1.1.0 -> 1.0.0", { checked: 4 });
expect(stderr).not.toContain("error:");
expect(exitCode).toBe(0);
expect(await lock(packageDir)).not.toContain('"[email protected]"');
});
// A direct dependency only moves down when that is the only way to drop a version (pnpm would keep both, pnpm/pnpm#4753, #6762).
test.concurrent("root range collapses onto a transitive exact pin", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
await write(
packageJson,
JSON.stringify({ name: "foo", dependencies: { "a-dep": "^1.0.0", "uses-a-dep-3": "1.0.0" } }),
);
await runBunInstall(installEnv(packageDir), packageDir);
const pkgJsonBefore = await file(packageJson).text();
const lockfile = await lock(packageDir);
expect(lockfile).toContain('"[email protected]"');
expect(lockfile).toContain('"[email protected]"');
expect(await nodeModulesVersion(packageDir, "a-dep")).toBe("1.0.10");
const { stdout, exitCode } = await dedupe(packageDir);
expectRemoved(stdout, "a-dep 1.0.10 -> 1.0.3");
expect(exitCode).toBe(0);
expect(await file(packageJson).text()).toBe(pkgJsonBefore);
const after = await lock(packageDir);
expect(after).toContain('"[email protected]"');
expect(after).not.toContain('"[email protected]"');
expect(await nodeModulesVersion(packageDir, "a-dep")).toBe("1.0.3");
await runBunInstall(installEnv(packageDir), packageDir, { frozenLockfile: true });
});
test.concurrent("npm: alias edges are deduplicated by the aliased range", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const lockfile = await installTwice(
packageDir,
packageJson,
{ name: "foo", dependencies: { "one-range-dep": "1.0.0" } },
{ name: "foo", dependencies: { "one-range-dep": "1.0.0", "nd": "npm:[email protected]" } },
);
expect(lockfile).toContain('"[email protected]"');
expect(lockfile).toContain('"[email protected]"');
const { stdout, exitCode } = await dedupe(packageDir);
expectRemoved(stdout, "no-deps 1.1.0 -> 1.0.0");
expect(exitCode).toBe(0);
expect(await lock(packageDir)).not.toContain('"[email protected]"');
expect(await nodeModulesVersion(packageDir, "nd")).toBe("1.0.0");
await runBunInstall(installEnv(packageDir), packageDir, { frozenLockfile: true });
});
test.concurrent("dist-tag edges keep the version the tag resolved to", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
await write(
packageJson,
JSON.stringify({ name: "foo", dependencies: { "dep-with-tags": "pre-1", "dwt": "npm:[email protected]" } }),
);
await runBunInstall(installEnv(packageDir), packageDir);
const lockBefore = await lock(packageDir);
expect(lockBefore).toContain('"[email protected]"');
expect(lockBefore).toContain('"[email protected]"');
await expectAlreadyDeduplicated(packageDir, 3);
expect(await lock(packageDir)).toBe(lockBefore);
expect(await nodeModulesVersion(packageDir, "dep-with-tags")).toBe("1.0.1");
});
test.concurrent("edges pointing at a patched version are never moved", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const lockBefore = await setupRangeDuplicate(packageDir, packageJson);
const patched = lockBefore.replace(
'\n "packages": {',
'\n "patchedDependencies": {\n "[email protected]": "patches/[email protected]",\n },\n "packages": {',
);
expect(patched).not.toBe(lockBefore);
await write(join(packageDir, "bun.lock"), patched);
await expectAlreadyDeduplicated(packageDir, 4);
expect(await lock(packageDir)).toBe(patched);
});
test.concurrent("--lockfile-only rewrites bun.lock without installing", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
await setupRangeDuplicate(packageDir, packageJson);
const nested = () => nodeModulesVersion(packageDir, "one-range-dep", "node_modules", "no-deps");
expect(await nested()).toBe("1.1.0");
const { stdout, stderr, exitCode } = await dedupe(packageDir, "--lockfile-only");
expect(lines(stdout)).toStrictEqual([
HEADER,
"~ no-deps 1.1.0 -> 1.0.0",
"",
"1 duplicate version removed (checked 4 packages)",
]);
expectRemoved(stdout, "no-deps 1.1.0 -> 1.0.0", { checked: 4 });
expect(lines(stderr)).toStrictEqual(["Saved lockfile"]);
expect(exitCode).toBe(0);
const lockDeduped = await lock(packageDir);
expect(lockDeduped).not.toContain('"[email protected]"');
expect(await nested()).toBe("1.1.0");
// Nothing left to remove: one line, and the unchanged lockfile is not saved again.
const again = await dedupe(packageDir, "--lockfile-only");
expectNoDuplicates(again, 3);
expect(again.stderr).toBe("");
expect(await lock(packageDir)).toBe(lockDeduped);
await runBunInstall(installEnv(packageDir), packageDir, { frozenLockfile: true });
});
test.concurrent("--silent keeps the exit codes", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const lockBefore = await setupRangeDuplicate(packageDir, packageJson);
const check = await dedupe(packageDir, "--check", "--silent");
expect(check.stdout).toBe("");
expect(check.stderr).toBe("");
expect(check.exitCode).toBe(1);
expect(await lock(packageDir)).toBe(lockBefore);
const dryRun = await dedupe(packageDir, "--dry-run", "--silent");
expect(dryRun.stdout).toBe("");
expect(dryRun.stderr).toBe("");
expect(dryRun.exitCode).toBe(0);
expect(await lock(packageDir)).toBe(lockBefore);
const apply = await dedupe(packageDir, "--silent");
expect(apply.stdout).toBe("");
expect(apply.stderr).toBe("");
expect(apply.exitCode).toBe(0);
expect(await lock(packageDir)).not.toContain('"[email protected]"');
});
// The workspace fixture: root pins [email protected], packages/a -> one-range-dep -> [email protected].
async function setupWorkspaceDuplicate(packageDir: string, packageJson: string, workspacePackageJson: object) {
const workspaceDir = join(packageDir, "packages", "a");
await write(join(workspaceDir, "package.json"), JSON.stringify(workspacePackageJson));
const lockfile = await installTwice(
packageDir,
packageJson,
{ name: "root", workspaces: ["packages/*"] },
{ name: "root", workspaces: ["packages/*"], dependencies: { "no-deps": "1.0.0" } },
);
expect(lockfile).toContain('"[email protected]"');
expect(lockfile).toContain('"[email protected]"');
return workspaceDir;
}
test.concurrent(
"hoisted: a workspace's collapsed nested copy is removed, foreign directories in the same folder are not",
async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const workspaceDir = join(packageDir, "packages", "a");
await write(
join(workspaceDir, "package.json"),
JSON.stringify({ name: "a", dependencies: { "no-deps": "^1.0.0" } }),
);
const lockfile = await installTwice(
packageDir,
packageJson,
{ name: "root", workspaces: ["packages/*"] },
{ name: "root", workspaces: ["packages/*"], dependencies: { "no-deps": "1.0.0" } },
);
expect(lockfile).toContain('"[email protected]"');
expect(lockfile).toContain('"[email protected]"');
const versions = () =>
Promise.all([nodeModulesVersion(packageDir, "no-deps"), nodeModulesVersion(workspaceDir, "no-deps")]);
expect(await versions()).toStrictEqual(["1.0.0", "1.1.0"]);
// Stand-ins for a manual `bun link` inside the workspace and an extraneous root package: bun.lock never placed them.
const handLinked = join(workspaceDir, "node_modules", "hand-linked", "package.json");
const extraneous = join(packageDir, "node_modules", "extraneous", "package.json");
await Promise.all([
mkdir(dirname(handLinked), { recursive: true }),
mkdir(dirname(extraneous), { recursive: true }),
]);
await Promise.all([write(handLinked, '{"name":"hand-linked"}'), write(extraneous, '{"name":"extraneous"}')]);
const { stdout, stderr, exitCode } = await dedupe(packageDir);
expectRemoved(stdout, "no-deps 1.1.0 -> 1.0.0");
expect(stderr).not.toContain("error:");
expect(exitCode).toBe(0);
expect(await lock(packageDir)).not.toContain('"[email protected]"');
expect(await exists(join(workspaceDir, "node_modules", "no-deps"))).toBeFalse();
expect(await Promise.all([exists(handLinked), exists(extraneous)])).toStrictEqual([true, true]);
expect(await nodeModulesVersion(packageDir, "no-deps")).toBe("1.0.0");
await expectAlreadyDeduplicated(packageDir);
await runBunInstall(installEnv(packageDir), packageDir, { frozenLockfile: true });
},
);
type StaleFixture = { packageDir: string; cwds: string[]; afterRefusal?: () => Promise<void> };
// Every edit is still satisfied by the locked versions, so the refusal is about the ranges, not about re-resolution.
test.concurrent.each<[string, () => Promise<StaleFixture>]>([
[
"a root dependency range",
async () => {
const { packageDir, packageJson } = await registry.createTestDir();
await setupRangeDuplicate(packageDir, packageJson);
await write(
packageJson,
JSON.stringify({ name: "foo", dependencies: { "one-range-dep": "1.0.0", "no-deps": "^1.0.0" } }),
);
const nested = () => nodeModulesVersion(packageDir, "one-range-dep", "node_modules", "no-deps");
expect(await nested()).toBe("1.1.0");
return {
packageDir,
cwds: [packageDir],
async afterRefusal() {
expect(await nested()).toBe("1.1.0");
// The current range lets root move up onto 1.1.0; the recorded exact range would have removed it instead.
await runBunInstall(installEnv(packageDir), packageDir);
const { stdout, stderr, exitCode } = await dedupe(packageDir);
expectRemoved(stdout, "no-deps 1.0.0 -> 1.1.0");
expect(stderr).not.toContain("error:");
expect(exitCode).toBe(0);
expect(await nodeModulesVersion(packageDir, "no-deps")).toBe("1.1.0");
expect(
await exists(join(packageDir, "node_modules", "one-range-dep", "node_modules", "no-deps")),
).toBeFalse();
},
};
},
],
[
"an override",
async () => {
const { packageDir, packageJson } = await registry.createTestDir();
await setupRangeDuplicate(packageDir, packageJson);
await write(
packageJson,
JSON.stringify({
name: "foo",
dependencies: { "one-range-dep": "1.0.0", "no-deps": "1.0.0" },
overrides: { "no-deps": "1.1.0" },
}),
);
return {
packageDir,
cwds: [packageDir],
async afterRefusal() {
await runBunInstall(installEnv(packageDir), packageDir);
const lockfile = await lock(packageDir);
expect(lockfile).toContain('"overrides"');
expect(lockfile).not.toContain('"[email protected]"');
await expectAlreadyDeduplicated(packageDir, 3);
},
};
},
],
[
"a catalog entry",
async () => {
const { packageDir, packageJson } = await registry.createTestDir();
// Same steps as 'honours catalog ranges', then the catalog entry is edited without installing.
const root = (catalogRange: string, dependencies?: Record<string, string>) =>
JSON.stringify({
name: "root",
workspaces: ["packages/*"],
catalog: { "no-deps": catalogRange },
dependencies,
});
await Promise.all([
write(packageJson, root("1.0.0")),
write(
join(packageDir, "packages", "a", "package.json"),
JSON.stringify({ name: "a", dependencies: { "no-deps": "catalog:" } }),
),
]);
await runBunInstall(installEnv(packageDir), packageDir);
await write(packageJson, root("^1.0.0"));
await runBunInstall(installEnv(packageDir), packageDir);
await write(packageJson, root("^1.0.0", { "no-deps": "1.1.0" }));
await runBunInstall(installEnv(packageDir), packageDir);
const lockfile = await lock(packageDir);
expect(lockfile).toContain('"[email protected]"');
expect(lockfile).toContain('"[email protected]"');
await write(packageJson, root(">=1.0.0", { "no-deps": "1.1.0" }));
return { packageDir, cwds: [packageDir] };
},
],
[
"a workspace package's dependencies",
async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const workspaceDir = await setupWorkspaceDuplicate(packageDir, packageJson, {
name: "a",
dependencies: { "one-range-dep": "1.0.0" },
});
await write(
join(workspaceDir, "package.json"),
JSON.stringify({ name: "a", dependencies: { "one-range-dep": "^1.0.0" } }),
);
return { packageDir, cwds: [packageDir, workspaceDir] };
},
],
])("refuses to dedupe when %s changed since the last install", async (_, setup) => {
const { packageDir, cwds, afterRefusal } = await setup();
const lockBefore = await lock(packageDir);
for (const cwd of cwds) {
await expectRefused(cwd, "--check");
expect(await lock(packageDir)).toBe(lockBefore);
await expectRefused(cwd);
expect(await lock(packageDir)).toBe(lockBefore);
}
await afterRefusal?.();
});
// The gate is the dependency diff: diffs the differ reports on every install of an in-sync tree must not refuse.
test.concurrent.each([
[
"trustedDependencies",
async () => {
const { packageDir, packageJson } = await registry.createTestDir();
await setupRangeDuplicate(packageDir, packageJson);
const pkg = await file(packageJson).json();
await write(packageJson, JSON.stringify({ ...pkg, trustedDependencies: ["no-deps"] }));
return packageDir;
},
],
[
"a workspace lifecycle script",
async () => {
const { packageDir, packageJson } = await registry.createTestDir();
await setupWorkspaceDuplicate(packageDir, packageJson, {
name: "a",
dependencies: { "one-range-dep": "1.0.0" },
scripts: { postinstall: "exit 0" },
});
return packageDir;
},
],
[
"package.json formatting",
async () => {
const { packageDir, packageJson } = await registry.createTestDir();
await setupRangeDuplicate(packageDir, packageJson);
await write(
packageJson,
JSON.stringify(
{ description: "reformatted", dependencies: { "no-deps": "1.0.0", "one-range-dep": "1.0.0" }, name: "foo" },
null,
2,
),
);
return packageDir;
},
],
])("still dedupes when only %s changed", async (_, setup) => {
const packageDir = await setup();
const { stdout, stderr, exitCode } = await dedupe(packageDir);
expectRemoved(stdout, "no-deps 1.1.0 -> 1.0.0");
expect(stderr).not.toContain("error:");
expect(exitCode).toBe(0);
expect(await lock(packageDir)).not.toContain('"[email protected]"');
await runBunInstall(installEnv(packageDir), packageDir, { frozenLockfile: true });
});
test.concurrent("a direct dependency is not moved when its version survives anyway", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const lockfile = await installTwice(
packageDir,
packageJson,
{ name: "foo", dependencies: { "no-deps": "1.0.1", "dwt": "npm:dep-with-tags@^1.0.0" } },
{
name: "foo",
dependencies: {
"no-deps": "^1.0.0",
"one-dep": "1.0.0",
"one-fixed-dep": "1.0.0",
"has-bin-entries": "1.0.0",
"dwt": "npm:dep-with-tags@^1.0.0",
"dwt0": "npm:[email protected]",
},
},
);
for (const label of ['"[email protected]"', '"[email protected]"', '"[email protected]"', '"[email protected]"']) {
expect(lockfile).toContain(label);
}
expect(await nodeModulesVersion(packageDir, "no-deps")).toBe("1.0.1");
const { stdout, stderr, exitCode } = await dedupe(packageDir);
expectRemoved(stdout, "dep-with-tags 1.0.1 -> 1.0.0");
expect(stderr).not.toContain("error:");
expect(exitCode).toBe(0);
const after = await lock(packageDir);
expect(after).toContain('"[email protected]"');
expect(after).toContain('"[email protected]"');
expect(after).not.toContain('"[email protected]"');
expect(await nodeModulesVersion(packageDir, "no-deps")).toBe("1.0.1");
expect(await nodeModulesVersion(packageDir, "dwt")).toBe("1.0.0");
await expectAlreadyDeduplicated(packageDir);
await runBunInstall(installEnv(packageDir), packageDir, { frozenLockfile: true });
});
// no-deps ends up as: direct ^1.0.0 @1.1.0, `<=1.0.1` @1.0.1, `>=1.0.1` @1.0.1, exact 1.0.0 — {1.0.0, 1.1.0} and {1.0.0, 1.0.1} are equally small covers; 1.0.1's two edges land on different survivors.
test.concurrent.each(["root", "workspace"])(
"prefers dropping the version that keeps the direct dependency in place (%s)",
async variant => {
const { packageDir, packageJson } = await registry.createTestDir();
const rootExtras = { "one-fixed-dep": "1.0.0", "one-dep": "1.0.0", "normal-dep-and-dev-dep": "1.0.0" };
if (variant === "root") {
await write(packageJson, JSON.stringify({ name: "root", dependencies: { "no-deps": "^1.0.0" } }));
await runBunInstall(installEnv(packageDir), packageDir);
await write(packageJson, JSON.stringify({ name: "root", dependencies: { "no-deps": "^1.0.0", ...rootExtras } }));
} else {
await Promise.all([
write(packageJson, JSON.stringify({ name: "root", workspaces: ["packages/*"] })),
write(
join(packageDir, "packages", "a", "package.json"),
JSON.stringify({ name: "a", dependencies: { "no-deps": "^1.0.0" } }),
),
]);
await runBunInstall(installEnv(packageDir), packageDir);
await write(packageJson, JSON.stringify({ name: "root", workspaces: ["packages/*"], dependencies: rootExtras }));
}
await runBunInstall(installEnv(packageDir), packageDir);
let lockfile = await lock(packageDir);
for (const label of ['"[email protected]"', '"[email protected]"', '"[email protected]"']) {
expect(lockfile).toContain(label);
}
lockfile = editLockEntry(lockfile, '"one-dep": ["[email protected]"', '"no-deps": "1.0.1"', '"no-deps": "<=1.0.1"');
lockfile = editLockEntry(
lockfile,
'"normal-dep-and-dev-dep": ["[email protected]"',
'"no-deps": "1.0.1"',
'"no-deps": ">=1.0.1"',
);
await write(join(packageDir, "bun.lock"), lockfile);
const checked = lockPackageCount(lockfile);
const check = await dedupe(packageDir, "--check");
expectWouldRemove(check, "no-deps 1.0.1 -> 1.0.0, 1.1.0", checked);
expect(check.exitCode).toBe(1);
const { stdout, stderr, exitCode } = await dedupe(packageDir);
expectRemoved(stdout, "no-deps 1.0.1 -> 1.0.0, 1.1.0", { checked });
expect(stderr).not.toContain("error:");
expect(exitCode).toBe(0);
const after = await lock(packageDir);
expect(after).toContain('"[email protected]"');
expect(after).toContain('"[email protected]"');
expect(after).not.toContain('"[email protected]"');
if (variant === "root") {
expect(await nodeModulesVersion(packageDir, "no-deps")).toBe("1.1.0");
}
await expectAlreadyDeduplicated(packageDir);
await runBunInstall(installEnv(packageDir), packageDir, { frozenLockfile: true });
},
);
// pnpm/pnpm#4753 direct-dependency weighting: when the direct edge's own version is dropped, it moves to the highest survivor.
test.concurrent("a direct range whose version is dropped moves up, not down", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
await Promise.all([
write(
packageJson,
JSON.stringify({
name: "root",
workspaces: ["packages/*"],
dependencies: { "a-dep": "1.0.5", "uses-a-dep-3": "1.0.0", "uses-a-dep-10": "1.0.0" },
}),
),
write(
join(packageDir, "packages", "b", "package.json"),
JSON.stringify({ name: "b", dependencies: { "a-dep": "1.0.3" } }),
),
]);
await runBunInstall(installEnv(packageDir), packageDir);
await widen(packageDir, packageJson, "a-dep", "^1.0.0");
const lockfile = await lock(packageDir);
for (const label of ['"[email protected]"', '"[email protected]"', '"[email protected]"']) {
expect(lockfile).toContain(label);
}
const nestedVersions = () =>
Promise.all([
nodeModulesVersion(packageDir, "a-dep"),
nodeModulesVersion(packageDir, "uses-a-dep-3", "node_modules", "a-dep"),
nodeModulesVersion(join(packageDir, "packages", "b"), "a-dep"),
]);
expect(await nestedVersions()).toStrictEqual(["1.0.5", "1.0.3", "1.0.3"]);
expect(await nodeModulesVersion(packageDir, "uses-a-dep-10", "node_modules", "a-dep")).toBe("1.0.10");
const checked = lockPackageCount(lockfile);
const check = await dedupe(packageDir, "--check");
expectWouldRemove(check, "a-dep 1.0.5 -> 1.0.10", checked);
expect(check.exitCode).toBe(1);
const { stdout, stderr, exitCode } = await dedupe(packageDir);
expectRemoved(stdout, "a-dep 1.0.5 -> 1.0.10", { checked });
expect(stderr).not.toContain("error:");
expect(exitCode).toBe(0);
const after = await lock(packageDir);
expect(after).toContain('"[email protected]"');
expect(after).toContain('"[email protected]"');
expect(after).not.toContain('"[email protected]"');
expect(after).not.toContain('"uses-a-dep-10/a-dep"');
// uses-a-dep-10 now loads the hoisted 1.0.10, so its nested copy goes; the rows that survived keep their folders.
expect(await exists(join(packageDir, "node_modules", "uses-a-dep-10", "node_modules", "a-dep"))).toBeFalse();
expect(await nestedVersions()).toStrictEqual(["1.0.10", "1.0.3", "1.0.3"]);
await expectAlreadyDeduplicated(packageDir);
await runBunInstall(installEnv(packageDir), packageDir, { frozenLockfile: true });
});
// [email protected] has more edges than root's 1.0.10, but both versions must stay, so removing [email protected] must not drag root down.
test.concurrent("removing one name does not downgrade an unrelated direct dependency", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
await write(
join(packageDir, "packages", "b", "package.json"),
JSON.stringify({ name: "b", dependencies: { "a-dep": "1.0.3" } }),
);
const rootDeps = { "a-dep": "^1.0.0", "uses-a-dep-3": "1.0.0", "uses-a-dep-10": "1.0.0", "one-range-dep": "1.0.0" };
const lockfile = await installTwice(
packageDir,
packageJson,
{ name: "root", workspaces: ["packages/*"], dependencies: rootDeps },
{ name: "root", workspaces: ["packages/*"], dependencies: { ...rootDeps, "no-deps": "1.0.0" } },
);
for (const label of ['"a-dep": ["[email protected]"', '"[email protected]"', '"[email protected]"', '"[email protected]"']) {
expect(lockfile).toContain(label);
}
expect(lockfile).not.toContain('"uses-a-dep-10/a-dep"');
const versions = () =>
Promise.all([
nodeModulesVersion(packageDir, "a-dep"),
nodeModulesVersion(packageDir, "uses-a-dep-3", "node_modules", "a-dep"),
nodeModulesVersion(join(packageDir, "packages", "b"), "a-dep"),
nodeModulesVersion(packageDir, "no-deps"),
]);
expect(await versions()).toStrictEqual(["1.0.10", "1.0.3", "1.0.3", "1.0.0"]);
const nestedNoDeps = join(packageDir, "node_modules", "one-range-dep", "node_modules", "no-deps");
expect(await nodeModulesVersion(packageDir, "one-range-dep", "node_modules", "no-deps")).toBe("1.1.0");
const { stdout, stderr, exitCode } = await dedupe(packageDir);
expectRemoved(stdout, "no-deps 1.1.0 -> 1.0.0");
expect(stderr).not.toContain("error:");
expect(exitCode).toBe(0);
const after = await lock(packageDir);
expect(after).toContain('"a-dep": ["[email protected]"');
expect(after).toContain('"[email protected]"');
expect(after).not.toContain('"uses-a-dep-10/a-dep"');
expect(after).not.toContain('"[email protected]"');
expect(await exists(nestedNoDeps)).toBeFalse();
expect(await versions()).toStrictEqual(["1.0.10", "1.0.3", "1.0.3", "1.0.0"]);
await expectAlreadyDeduplicated(packageDir);
await runBunInstall(installEnv(packageDir), packageDir, { frozenLockfile: true });
});
test.concurrent("a workspace member's own range moves up when run from the member directory", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const memberDir = join(packageDir, "packages", "b");
const memberPackageJson = join(memberDir, "package.json");
await Promise.all([
write(
packageJson,
JSON.stringify({
name: "root",
workspaces: ["packages/*"],
dependencies: { "a-dep": "1.0.3", "uses-a-dep-3": "1.0.0", "uses-a-dep-10": "1.0.0" },
}),
),
write(memberPackageJson, JSON.stringify({ name: "b", dependencies: { "a-dep": "1.0.5" } })),
]);
await runBunInstall(installEnv(packageDir), packageDir);
await widen(packageDir, memberPackageJson, "a-dep", "^1.0.0");
const lockfile = await lock(packageDir);
for (const label of ['"a-dep": ["[email protected]"', '"b/a-dep": ["[email protected]"', '"[email protected]"']) {
expect(lockfile).toContain(label);
}
expect(await nodeModulesVersion(memberDir, "a-dep")).toBe("1.0.5");
const { stdout, stderr, exitCode } = await dedupe(memberDir);
expectRemoved(stdout, "a-dep 1.0.5 -> 1.0.10");
expect(stderr).not.toContain("error:");
expect(exitCode).toBe(0);
const after = await lock(packageDir);
expect(after).toContain('"a-dep": ["[email protected]"');
expect(after).toContain('"b/a-dep": ["[email protected]"');
expect(after).not.toContain('"[email protected]"');
expect(await nodeModulesVersion(memberDir, "a-dep")).toBe("1.0.10");
expect(await exists(join(memberDir, "bun.lock"))).toBeFalse();
await runBunInstall(installEnv(packageDir), packageDir, { frozenLockfile: true });
});
// dep-with-tags' `latest` tag is 3.0.0, so a fresh range would resolve there too; pin 3.0.1 first, then widen.
test.concurrent("ranges collapse onto the version a dist-tag resolved to", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
await write(
packageJson,
JSON.stringify({ name: "foo", dependencies: { "dwt": "npm:[email protected]", "dep-with-tags": "latest" } }),
);
await runBunInstall(installEnv(packageDir), packageDir);
await widen(packageDir, packageJson, "dwt", "npm:dep-with-tags@>=1.0.0");
const lockfile = await lock(packageDir);
expect(lockfile).toContain('"[email protected]"');
expect(lockfile).toContain('"[email protected]"');
const { stdout, stderr, exitCode } = await dedupe(packageDir);
expectRemoved(stdout, "dep-with-tags 3.0.1 -> 3.0.0");
expect(stderr).not.toContain("error:");
expect(exitCode).toBe(0);
expect(await lock(packageDir)).not.toContain('"[email protected]"');
expect(await nodeModulesVersion(packageDir, "dep-with-tags")).toBe("3.0.0");
expect(await nodeModulesVersion(packageDir, "dwt")).toBe("3.0.0");
await runBunInstall(installEnv(packageDir), packageDir, { frozenLockfile: true });
});
// The patched version pins root's now-ranged direct edge, so the transitive edge collapses down onto it.
test.concurrent("a patched version wins over keeping the direct dependency's higher version", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
await write(join(packageDir, "patches", "[email protected]"), noDepsPatch);
await installTwice(
packageDir,
packageJson,
{ name: "foo", dependencies: { "one-range-dep": "1.0.0" } },
{
name: "foo",
dependencies: { "no-deps": "1.0.0", "one-range-dep": "1.0.0" },
patchedDependencies: { "[email protected]": "patches/[email protected]" },
},
);
await widen(packageDir, packageJson, "no-deps", "^1.0.0");
const lockfile = await lock(packageDir);
expect(lockfile).toContain('"[email protected]": "patches/[email protected]"');
expect(lockfile).toContain('"[email protected]"');
expect(await exists(join(packageDir, "node_modules", "no-deps", "patched.txt"))).toBeTrue();
const { stdout, stderr, exitCode } = await dedupe(packageDir);
expectRemoved(stdout, "no-deps 1.1.0 -> 1.0.0");
expect(stderr).not.toContain("error:");
expect(exitCode).toBe(0);
const after = await lock(packageDir);
expect(after).toContain('"[email protected]": "patches/[email protected]"');
expect(after).toContain('"no-deps": ["[email protected]"');
expect(after).not.toContain('"[email protected]"');
expect(await nodeModulesVersion(packageDir, "no-deps")).toBe("1.0.0");
expect(await exists(join(packageDir, "node_modules", "no-deps", "patched.txt"))).toBeTrue();
await runBunInstall(installEnv(packageDir), packageDir, { frozenLockfile: true });
});
// [email protected] is the only dependent of the patched [email protected], so root's ">=1.0.0" edge must not move up onto 2.0.0.
test.concurrent("a version that is the only way to reach a patched package is kept and reported", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
await write(join(packageDir, "patches", "[email protected]"), noDepsPatch);
const patched = { "[email protected]": "patches/[email protected]" };
await installTwice(
packageDir,
packageJson,
{
name: "foo",
dependencies: { "one-fixed-dep": "1.0.0", "dwt": "npm:[email protected]" },
patchedDependencies: patched,
},
{
name: "foo",
dependencies: {
"one-fixed-dep": ">=1.0.0",
"ofd2": "npm:[email protected]",
"dwt": "npm:[email protected]",
"dep-with-tags": "latest",
},
patchedDependencies: patched,
},
);
await widen(packageDir, packageJson, "dwt", "npm:dep-with-tags@>=1.0.0");
const lockfile = await lock(packageDir);
for (const label of [
'"[email protected]"',
'"[email protected]"',
'"[email protected]"',
'"[email protected]"',
'"[email protected]"',
'"[email protected]"',
'"[email protected]": "patches/[email protected]"',
]) {
expect(lockfile).toContain(label);
}
expect(lockPackageCount(lockfile)).toBe(7);
const kept = [" kept [email protected] (needed to reach patched [email protected])"];
// Rows first (the kept row belongs with them), then the one summary line, then the hint.
const check = await dedupe(packageDir, "--check");
expect(lines(check.stdout)).toMatchInlineSnapshot(`
[
"bun dedupe <version> (<revision>)",
"~ dep-with-tags 3.0.1 -> 3.0.0",
" kept [email protected] (needed to reach patched [email protected])",
"",
"1 duplicate version can be removed (checked 7 packages)",
" bun dedupe",
]
`);
expectWouldRemove(check, "dep-with-tags 3.0.1 -> 3.0.0", 7, kept);
expect(check.stderr).toBe("");
expect(check.exitCode).toBe(1);
expect(await lock(packageDir)).toBe(lockfile);
const frozen = await dedupe(packageDir, "--frozen-lockfile");
expectRefusedToWrite(frozen, "dep-with-tags 3.0.1 -> 3.0.0", 7, "--frozen-lockfile was passed", kept);
expect(await lock(packageDir)).toBe(lockfile);
const { stdout, stderr, exitCode } = await dedupe(packageDir);
expectRemoved(stdout, "dep-with-tags 3.0.1 -> 3.0.0", { kept, checked: 7 });
expect(stderr).not.toContain("kept");
expect(stderr).not.toContain("error:");
expect(exitCode).toBe(0);
const after = await lock(packageDir);
expect(after).toContain('"[email protected]"');
expect(after).toContain('"[email protected]"');
expect(after).toContain('"[email protected]": "patches/[email protected]"');
expect(after).not.toContain('"[email protected]"');
expect(await nodeModulesVersion(packageDir, "one-fixed-dep")).toBe("1.0.0");
expect(await nodeModulesVersion(packageDir, "dep-with-tags")).toBe("3.0.0");
expect(lockPackageCount(after)).toBe(6);
const recheck = await dedupe(packageDir, "--check");
expect(lines(recheck.stdout)).toMatchInlineSnapshot(`
[
"bun dedupe <version> (<revision>)",
" kept [email protected] (needed to reach patched [email protected])",
"",
"🎉 No duplicates — checked 6 packages, every one already resolves to a single version",
]
`);
expectNoDuplicates(recheck, 6, kept);
expectNoDuplicates(await dedupe(packageDir), 6, kept);
expect(await lock(packageDir)).toBe(after);
await runBunInstall(installEnv(packageDir), packageDir, { frozenLockfile: true });
});
// pnpm/pnpm#9213 (and #6619): [email protected] dies in this run, so its exact [email protected] edge must not drag the live "^1.0.0" edges down.
test.concurrent("a version removed by the run does not vote for its own dependencies", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
await write(
packageJson,
JSON.stringify({ name: "foo", dependencies: { "one-range-dep": "1.0.0", "no-deps": "^1.0.0" } }),
);
await runBunInstall(installEnv(packageDir), packageDir);
expect(await lock(packageDir)).toContain('"[email protected]"');
const lockfile = await installTwice(
packageDir,
packageJson,
{ name: "foo", dependencies: { "one-range-dep": "1.0.0", "no-deps": "^1.0.0", "one-fixed-dep": "1.0.0" } },
{
name: "foo",
dependencies: {
"one-range-dep": "1.0.0",
"no-deps": "^1.0.0",
"one-fixed-dep": ">=1.0.0",
"ofd2": "npm:[email protected]",
},
},
);
for (const label of [
'"[email protected]"',
'"[email protected]"',
'"[email protected]"',
'"[email protected]"',
'"[email protected]"',
]) {
expect(lockfile).toContain(label);
}
expect(await nodeModulesVersion(packageDir, "no-deps")).toBe("1.1.0");
const rows = ["no-deps 1.0.0", "one-fixed-dep 1.0.0 -> 2.0.0"];
const checked = lockPackageCount(lockfile);
expect(checked).toBe(7);
const check = await dedupe(packageDir, "--check");
expectWouldRemove(check, rows, checked);
expect(check.exitCode).toBe(1);
const { stdout, stderr, exitCode } = await dedupe(packageDir);
expectRemoved(stdout, rows, { checked });
expect(stderr).not.toContain("error:");
expect(exitCode).toBe(0);
const after = await lock(packageDir);
expect(after).toContain('"[email protected]"');
expect(after).toContain('"[email protected]"');
expect(after).toContain('"[email protected]"');
expect(after).not.toContain('"[email protected]"');
expect(after).not.toContain('"[email protected]"');
expect(await nodeModulesVersion(packageDir, "no-deps")).toBe("1.1.0");
expect(await nodeModulesVersion(packageDir, "one-fixed-dep")).toBe("2.0.0");
await expectAlreadyDeduplicated(packageDir, 5);
expect(await lock(packageDir)).toBe(after);
await runBunInstall(installEnv(packageDir), packageDir, { frozenLockfile: true });
});
// A bundled edge is satisfied by the tarball's own copy, so it stays put and root's range collapses onto it instead.
test.concurrent("bundled edges are never re-pointed", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const lockfile = await installTwice(
packageDir,
packageJson,
{ name: "foo", dependencies: { "no-deps": "^1.0.0" } },
{ name: "foo", dependencies: { "no-deps": "^1.0.0", "bundled-1": "1.0.0" } },
);
expect(lockfile).toContain('"bundled-1/no-deps": ["[email protected]"');
expect(lockfile).toContain('"bundled": true');
expect(lockfile).toContain('{ "dependencies": { "no-deps": "1.0.0" } }');
const widened = lockfile.replace(
'{ "dependencies": { "no-deps": "1.0.0" } }',
'{ "dependencies": { "no-deps": "^1.0.0" } }',
);
await write(join(packageDir, "bun.lock"), widened);
const checked = lockPackageCount(widened);
const check = await dedupe(packageDir, "--check");
expectWouldRemove(check, "no-deps 1.1.0 -> 1.0.0", checked);
expect(check.exitCode).toBe(1);
expect(await lock(packageDir)).toBe(widened);
const { stdout, stderr, exitCode } = await dedupe(packageDir, "--lockfile-only");
expectRemoved(stdout, "no-deps 1.1.0 -> 1.0.0", { checked });
expect(stderr).not.toContain("error:");
expect(exitCode).toBe(0);
const after = await lock(packageDir);
expect(after).toContain('"bundled-1/no-deps": ["[email protected]"');
expect(after).toContain('"bundled": true');
expect(after).not.toContain('"[email protected]"');
});
// pnpm/pnpm#11238, #10329, #8446: dedupe never re-resolves, so it sends no registry request and ignores minimumReleaseAge.
test.concurrent("does not contact the registry", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
await setupRangeDuplicate(packageDir, packageJson);
const requests: string[] = [];
using server = Bun.serve({
port: 0,
fetch(req) {
requests.push(new URL(req.url).pathname);
return new Response("registry must not be contacted", { status: 500 });
},
});
await write(
join(packageDir, "bunfig.toml"),
Bun.TOML.stringify({
install: {
cache: join(packageDir, ".bun-cache"),
registry: `http://localhost:${server.port}/`,
minimumReleaseAge: 60 * 60 * 24 * 365 * 100,
},
}),
);
const check = await dedupe(packageDir, "--check");
expectWouldRemove(check, "no-deps 1.1.0 -> 1.0.0", 4);
expect(check.exitCode).toBe(1);
const apply = await dedupe(packageDir, "--lockfile-only");
expectRemoved(apply.stdout, "no-deps 1.1.0 -> 1.0.0", { checked: 4 });
expect(apply.stderr).toContain("Saved lockfile");
expect(apply.stderr).not.toContain("error:");
expect(apply.exitCode).toBe(0);
expect(await lock(packageDir)).not.toContain('"[email protected]"');
await expectAlreadyDeduplicated(packageDir, 3);
expect(requests).toStrictEqual([]);
});
// The tarball and the folder both unpack to a package named no-deps, so they share the npm versions' package_index entry.
test.concurrent.each<[string, (packageDir: string) => Promise<string>, string]>([
[
"a local tarball",
async packageDir => {
await copyFile(
join(registry.packagesPath, "no-deps", "no-deps-2.0.0.tgz"),
join(packageDir, "no-deps-2.0.0.tgz"),
);
return "file:./no-deps-2.0.0.tgz";
},
"no-deps-2.0.0.tgz",
],
[
"a folder",
async packageDir => {
await write(join(packageDir, "vendored", "package.json"), JSON.stringify({ name: "no-deps", version: "2.0.0" }));
return "file:./vendored";
},
"vendored",
],
])("a non-registry resolution of the same name is left alone (%s)", async (_, prepare, resolutionSuffix) => {
const { packageDir, packageJson } = await registry.createTestDir();
const spec = await prepare(packageDir);
const lockfile = await installTwice(
packageDir,
packageJson,
{ name: "foo", dependencies: { "one-range-dep": "1.0.0", "nd-local": spec } },
{ name: "foo", dependencies: { "one-range-dep": "1.0.0", "nd-local": spec, "no-deps": "1.0.0" } },
);
expect(lockfile).toContain('"[email protected]"');
expect(lockfile).toContain('"[email protected]"');
const localEntry = lockfile.split("\n").find(line => line.trimStart().startsWith('"nd-local": ["no-deps@'));
expect(localEntry).toContain(resolutionSuffix);
expect(await nodeModulesVersion(packageDir, "nd-local")).toBe("2.0.0");
// The non-registry copy is checked (it is a lockfile package) but never a candidate.
const checked = lockPackageCount(lockfile);
expect(checked).toBe(5);
const check = await dedupe(packageDir, "--check");
expectWouldRemove(check, "no-deps 1.1.0 -> 1.0.0", checked);
expect(check.exitCode).toBe(1);
const { stdout, stderr, exitCode } = await dedupe(packageDir);
expectRemoved(stdout, "no-deps 1.1.0 -> 1.0.0", { checked });
expect(stderr).not.toContain("error:");
expect(exitCode).toBe(0);
const after = await lock(packageDir);
expect(after).toContain('"[email protected]"');
expect(after).not.toContain('"[email protected]"');
expect(after.split("\n")).toContain(localEntry);
expect(await nodeModulesVersion(packageDir, "nd-local")).toBe("2.0.0");
expect(await nodeModulesVersion(packageDir, "no-deps")).toBe("1.0.0");
await expectAlreadyDeduplicated(packageDir, 4);
await runBunInstall(installEnv(packageDir), packageDir, { frozenLockfile: true });
});
// The installer skips overrides for `npm:` aliases, so the alias keeps 1.0.0 while the override holds one-range-dep's edge on 1.1.0.
test.concurrent("an npm: alias edge is not subject to the override for the aliased name", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
await write(
packageJson,
JSON.stringify({
name: "foo",
dependencies: { "one-range-dep": "1.0.0", "nd": "npm:[email protected]" },
overrides: { "no-deps": "1.1.0" },
}),
);
await runBunInstall(installEnv(packageDir), packageDir);
const lockBefore = await lock(packageDir);
expect(lockBefore).toContain('"[email protected]"');
expect(lockBefore).toContain('"[email protected]"');
expect(await nodeModulesVersion(packageDir, "nd")).toBe("1.0.0");
expect(await nodeModulesVersion(packageDir, "no-deps")).toBe("1.1.0");
await expectAlreadyDeduplicated(packageDir, 4);
expect(await lock(packageDir)).toBe(lockBefore);
expect(await nodeModulesVersion(packageDir, "nd")).toBe("1.0.0");
expect(await nodeModulesVersion(packageDir, "no-deps")).toBe("1.1.0");
await runBunInstall(installEnv(packageDir), packageDir, { frozenLockfile: true });
});
// `bun install` deletes an empty lockfile instead of writing one, so this shape only comes from a hand-written bun.lock.
const emptyRootLockfile = `{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "foo",
},
},
"packages": {}
}
`;
test.concurrent("a lockfile whose root has no dependencies is reported as deduplicated", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
await Promise.all([
write(packageJson, JSON.stringify({ name: "foo" })),
write(join(packageDir, "bun.lock"), emptyRootLockfile),
]);
const lockBefore = emptyRootLockfile;
// The root itself is the one package checked, like every other summary in the install family counts it.
const check = await dedupe(packageDir, "--check");
expect(lines(check.stdout)).toMatchInlineSnapshot(`
[
"bun dedupe <version> (<revision>)",
"🎉 No duplicates — checked 1 package, every one already resolves to a single version",
]
`);
expectNoDuplicates(check, 1);
expect(await lock(packageDir)).toBe(lockBefore);
const apply = await dedupe(packageDir);
expectNoDuplicates(apply, 1);
expect(apply.stderr).toBe("");
expect(await exists(join(packageDir, "bun.lock"))).toBeTrue();
expect(await lock(packageDir)).toBe(lockBefore);
});
test.concurrent("refuses when every dependency was removed from package.json since the last install", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const lockBefore = await setupRangeDuplicate(packageDir, packageJson);
await write(packageJson, JSON.stringify({ name: "foo" }));
await expectRefused(packageDir, "--check");
await expectRefused(packageDir);
expect(await lock(packageDir)).toBe(lockBefore);
});
// The docs' out-of-date rule applies in this direction too; resolving the new dependency would be `bun install`'s job.
test.concurrent("refuses when dependencies were added to a package.json whose lockfile has none", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
await Promise.all([
write(packageJson, JSON.stringify({ name: "foo", dependencies: { "no-deps": "1.0.0" } })),
write(join(packageDir, "bun.lock"), emptyRootLockfile),
]);
const lockBefore = emptyRootLockfile;
await expectRefused(packageDir, "--check");
expect(await lock(packageDir)).toBe(lockBefore);
await expectRefused(packageDir);
expect(await lock(packageDir)).toBe(lockBefore);
expect(await exists(join(packageDir, "node_modules", "no-deps"))).toBeFalse();
});
const npmLockWithDuplicate = JSON.stringify({
name: "foo",
lockfileVersion: 3,
requires: true,
packages: {
"": { name: "foo", dependencies: { "no-deps": "1.0.0", "one-range-dep": "1.0.0" } },
"node_modules/no-deps": { version: "1.0.0" },
"node_modules/one-range-dep": { version: "1.0.0", dependencies: { "no-deps": "^1.0.0" } },
"node_modules/one-range-dep/node_modules/no-deps": { version: "1.1.0" },
},
});
// Like every other install command, a foreign lockfile is migrated in memory before the missing-lockfile check.
test.concurrent("migrates package-lock.json when there is no bun.lock", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
await Promise.all([
write(packageJson, JSON.stringify({ name: "foo", dependencies: { "no-deps": "1.0.0", "one-range-dep": "1.0.0" } })),
write(join(packageDir, "package-lock.json"), npmLockWithDuplicate),
]);
const check = await dedupe(packageDir, "--check");
expectWouldRemove(check, "no-deps 1.1.0 -> 1.0.0", 4);
expect(check.stderr).toContain("migrated lockfile from package-lock.json");
expect(check.exitCode).toBe(1);
expect(await exists(join(packageDir, "bun.lock"))).toBeFalse();
// Nothing was installed before this run, so the summary also carries the install count.
const { stdout, stderr, exitCode } = await dedupe(packageDir);
expectRemoved(stdout, "no-deps 1.1.0 -> 1.0.0", { checked: 4 });
expect(lines(stdout).at(-1)).toBe("1 duplicate version removed, 2 packages installed (checked 4 packages)");
expect(stderr).toContain("migrated lockfile from package-lock.json");
expect(stderr).toContain("Saved lockfile");
expect(stderr).not.toContain("error:");
expect(exitCode).toBe(0);
const after = await lock(packageDir);
expect(after).toContain('"[email protected]"');
expect(after).not.toContain('"[email protected]"');
expect(await nodeModulesVersion(packageDir, "no-deps")).toBe("1.0.0");
expect(await exists(join(packageDir, "node_modules", "one-range-dep", "node_modules", "no-deps"))).toBeFalse();
await expectAlreadyDeduplicated(packageDir, 3);
await runBunInstall(installEnv(packageDir), packageDir, { frozenLockfile: true });
});
test.concurrent("corrupt package-lock.json fails without writing bun.lock", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
await Promise.all([
write(packageJson, JSON.stringify({ name: "foo", dependencies: { "no-deps": "1.0.0" } })),
write(join(packageDir, "package-lock.json"), "{ not valid"),
]);
for (const args of [[], ["--check"]]) {
const { stdout, stderr, exitCode } = await dedupe(packageDir, ...args);
expect(stderr).toContain("error: failed to migrate lockfile: ");
expect(stderr).not.toContain("Saved lockfile");
expect(stdout).not.toContain("duplicate");
expect(exitCode).not.toBe(0);
expect(await exists(join(packageDir, "bun.lock"))).toBeFalse();
expect(await exists(join(packageDir, "node_modules"))).toBeFalse();
}
});
test.concurrent("a bun.lock that cannot be read fails without touching it", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
await Promise.all([
write(packageJson, JSON.stringify({ name: "foo", dependencies: { "no-deps": "1.0.0" } })),
mkdir(join(packageDir, "bun.lock")),
]);
for (const args of [[], ["--check"]]) {
const { stdout, stderr, exitCode } = await dedupe(packageDir, ...args);
expect(stderr).toMatch(/error: failed to (open|read) lockfile: /);
expect(stderr).not.toContain("missing lockfile");
expect(stdout).not.toContain("duplicate");
expect(exitCode).not.toBe(0);
expect(await readdirSorted(join(packageDir, "bun.lock"))).toStrictEqual([]);
expect(await exists(join(packageDir, "node_modules"))).toBeFalse();
}
});
test.concurrent("--silent prints nothing on the error and no-op paths either", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const lockBefore = await setupRangeDuplicate(packageDir, packageJson);
const frozen = await dedupe(packageDir, "--frozen-lockfile", "--silent");
expect(frozen.stdout).toBe("");
expect(frozen.stderr).toBe("");
expect(frozen.exitCode).toBe(1);
expect(await lock(packageDir)).toBe(lockBefore);
const apply = await dedupe(packageDir, "--silent");
expect(apply.stdout).toBe("");
expect(apply.stderr).toBe("");
expect(apply.exitCode).toBe(0);
const lockDeduped = await lock(packageDir);
expect(lockDeduped).not.toContain('"[email protected]"');
for (const args of [["--check"], []]) {
const clean = await dedupe(packageDir, ...args, "--silent");
expect(clean.stdout).toBe("");
expect(clean.stderr).toBe("");
expect(clean.exitCode).toBe(0);
expect(await lock(packageDir)).toBe(lockDeduped);
}
});
test.concurrent("--silent prints nothing when refusing either", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const lockBefore = await setupRangeDuplicate(packageDir, packageJson);
await write(
packageJson,
JSON.stringify({ name: "foo", dependencies: { "one-range-dep": "1.0.0", "no-deps": "^1.0.0" } }),
);
for (const args of [["--check"], []]) {
const stale = await dedupe(packageDir, ...args, "--silent");
expect(stale.stdout).toBe("");
expect(stale.stderr).toBe("");
expect(stale.exitCode).toBe(1);
expect(await lock(packageDir)).toBe(lockBefore);
}
await rm(join(packageDir, "bun.lock"));
for (const args of [["--check"], []]) {
const missing = await dedupe(packageDir, ...args, "--silent");
expect(missing.stdout).toBe("");
expect(missing.stderr).toBe("");
expect(missing.exitCode).toBe(1);
expect(await exists(join(packageDir, "bun.lock"))).toBeFalse();
}
});
test.concurrent(
"bun dedupe wins over a package.json script named dedupe; bun run dedupe still runs the script",
async () => {
const { packageDir, packageJson } = await registry.createTestDir();
await setupRangeDuplicate(packageDir, packageJson);
const pkg = await file(packageJson).json();
await write(packageJson, JSON.stringify({ ...pkg, scripts: { dedupe: "echo SCRIPT_RAN" } }));
const { stdout, stderr, exitCode } = await dedupe(packageDir);
expectRemoved(stdout, "no-deps 1.1.0 -> 1.0.0");
expect(stdout).not.toContain("SCRIPT_RAN");
expect(stderr).not.toContain("SCRIPT_RAN");
expect(exitCode).toBe(0);
expect(await lock(packageDir)).not.toContain('"[email protected]"');
const script = await run(packageDir, "run", "dedupe");
expect(script.stdout).toContain("SCRIPT_RAN");
expect(script.exitCode).toBe(0);
},
);
test.concurrent("errors without a package.json", async () => {
const { packageDir } = await registry.createTestDir();
for (const args of [[], ["--check"]]) {
const { stdout, stderr, exitCode } = await dedupe(packageDir, ...args);
expect(stderr).toContain("error: missing package.json, nothing to dedupe");
expect(stdout).not.toContain("duplicate");
expect(exitCode).toBe(1);
}
expect(await readdirSorted(packageDir)).toStrictEqual(["bunfig.toml"]);
});
test.concurrent("exits 1 and keeps bun.lock when the install after deduplicating fails", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const lockfile = await setupRangeDuplicate(packageDir, packageJson);
using server = Bun.serve({
port: 0,
fetch: () => new Response("tarballs are unavailable", { status: 500 }),
});
// Tarball URLs are recorded per package, so the lockfile is what points the install at the failing server.
expect(lockfile).toContain(registry.registryUrl());
await Promise.all([
rm(join(packageDir, "node_modules"), { recursive: true }),
rm(join(packageDir, ".bun-cache"), { recursive: true }),
write(
join(packageDir, "bun.lock"),
lockfile.replaceAll(registry.registryUrl(), `http://localhost:${server.port}/`),
),
]);
const { stdout, stderr, exitCode } = await dedupe(packageDir);
expect(normalizeBunSnapshot(stdout).split("\n")[0]).toBe(HEADER);
// The report is only printed once the install finishes, so a failed run never claims a removal.
expect(stdout).not.toContain("Removed");
expect(stderr).toContain("error:");
expect(exitCode).toBe(1);
// A failed install persists nothing: bun.lock still holds both versions and can be deduplicated again.
expect(await lock(packageDir)).toContain('"[email protected]"');
expect(await exists(join(packageDir, "node_modules", "no-deps", "package.json"))).toBeFalse();
});
// Two dropped versions each guard patched packages: [email protected] leads to two of them, [email protected] to one; [email protected] is the removable duplicate.
test.concurrent("several kept versions are listed in name order with their patched packages joined", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const patchedDependencies = {
"[email protected]": "patches/[email protected]",
"[email protected]": "patches/[email protected]",
"[email protected]": "patches/[email protected]",
};
await Promise.all(Object.values(patchedDependencies).map(path => write(join(packageDir, path), noDepsPatch)));
await installTwice(
packageDir,
packageJson,
{
name: "foo",
dependencies: {
"one-fixed-dep": "1.0.0",
"optional-peer-hoist-deep-child": "1.0.0",
"dwt": "npm:[email protected]",
},
patchedDependencies,
},
{
name: "foo",
dependencies: {
"one-fixed-dep": ">=1.0.0",
"ofd2": "npm:[email protected]",
"optional-peer-hoist-deep-child": ">=1.0.0",
"dc2": "npm:[email protected]",
"dwt": "npm:[email protected]",
"dep-with-tags": "latest",
},
patchedDependencies,
},
);
await widen(packageDir, packageJson, "dwt", "npm:dep-with-tags@>=1.0.0");
const lockfile = await lock(packageDir);
for (const label of [
'"[email protected]"',
'"[email protected]"',
'"[email protected]"',
'"[email protected]"',
'"[email protected]"',
'"[email protected]"',
'"[email protected]"',
'"[email protected]"',
'"[email protected]"',
]) {
expect(lockfile).toContain(label);
}
const keptLines = [
" kept [email protected] (needed to reach patched [email protected])",
" kept [email protected] (needed to reach patched [email protected], [email protected])",
];
const checked = lockPackageCount(lockfile);
const check = await dedupe(packageDir, "--check");
expectWouldRemove(check, "dep-with-tags 3.0.1 -> 3.0.0", checked, keptLines);
expect(check.stderr).toBe("");
expect(check.exitCode).toBe(1);
expect(await lock(packageDir)).toBe(lockfile);
for (const [flag, reason] of REFUSALS) {
const refused = await dedupe(packageDir, flag);
expectRefusedToWrite(refused, "dep-with-tags 3.0.1 -> 3.0.0", checked, reason, keptLines);
expect(await lock(packageDir)).toBe(lockfile);
}
const { stdout, stderr, exitCode } = await dedupe(packageDir);
expectRemoved(stdout, "dep-with-tags 3.0.1 -> 3.0.0", { kept: keptLines, checked });
expect(stderr).not.toContain("kept");
expect(stderr).not.toContain("error:");
expect(exitCode).toBe(0);
const after = await lock(packageDir);
for (const label of [
'"[email protected]"',
'"[email protected]"',
'"[email protected]"',
'"[email protected]"',
'"[email protected]"',
]) {
expect(after).toContain(label);
}
expect(after).not.toContain('"[email protected]"');
expect(await nodeModulesVersion(packageDir, "dwt")).toBe("3.0.0");
expect(lockPackageCount(after)).toBe(checked - 1);
await expectAlreadyDeduplicated(packageDir, checked - 1, keptLines);
expect(await lock(packageDir)).toBe(after);
await runBunInstall(installEnv(packageDir), packageDir, { frozenLockfile: true });
});
test.concurrent("--check combined with --frozen-lockfile still only reports", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const lockBefore = await setupRangeDuplicate(packageDir, packageJson);
const check = await dedupe(packageDir, "--check", "--frozen-lockfile");
expectWouldRemove(check, "no-deps 1.1.0 -> 1.0.0", 4);
expect(check.stderr).toBe("");
expect(check.exitCode).toBe(1);
expect(await lock(packageDir)).toBe(lockBefore);
});
// [email protected] is stored twice (once per no-deps it is peered with); dedupe only counts versions.
test.concurrent("isolated: --check ignores peer variants of one version", async () => {
const { packageDir, packageJson } = await registry.createTestDir({ bunfigOpts: { linker: "isolated" } });
await write(
packageJson,
JSON.stringify({
name: "foo",
dependencies: { "provides-peer-deps-1-0-0": "1.0.0", "provides-peer-deps-2-0-0": "1.0.0" },
}),
);
await runBunInstall(installEnv(packageDir), packageDir);
const lockBefore = await lock(packageDir);
const store = await readdirSorted(join(packageDir, "node_modules", ".bun"));
expect(store.filter(entry => entry.startsWith("[email protected]"))).toHaveLength(2);
expect(lockPackageCount(lockBefore)).toBe(6);
for (const args of [["--check"], []]) {
const result = await dedupe(packageDir, ...args, "--linker", "isolated");
expectNoDuplicates(result, 6);
expect(result.stderr).toBe("");
expect(await lock(packageDir)).toBe(lockBefore);
expect(await readdirSorted(join(packageDir, "node_modules", ".bun"))).toStrictEqual(store);
}
});
test.concurrent("isolated: --check reports duplicates without touching the store", async () => {
const { packageDir, packageJson } = await registry.createTestDir({ bunfigOpts: { linker: "isolated" } });
const install = () => run(packageDir, "install", "--linker", "isolated");
await write(packageJson, JSON.stringify({ name: "foo", dependencies: { "one-range-dep": "1.0.0" } }));
expect((await install()).exitCode).toBe(0);
await write(
packageJson,
JSON.stringify({ name: "foo", dependencies: { "one-range-dep": "1.0.0", "no-deps": "1.0.0" } }),
);
expect((await install()).exitCode).toBe(0);
const lockBefore = await lock(packageDir);
const store = await readdirSorted(join(packageDir, "node_modules", ".bun"));
expect(store).toContain("[email protected]");
expect(store).toContain("[email protected]");
const check = await dedupe(packageDir, "--check", "--linker", "isolated");
expectWouldRemove(check, "no-deps 1.1.0 -> 1.0.0", 4);
expect(check.exitCode).toBe(1);
expect(await lock(packageDir)).toBe(lockBefore);
expect(await readdirSorted(join(packageDir, "node_modules", ".bun"))).toStrictEqual(store);
const dryRun = await dedupe(packageDir, "--dry-run", "--linker", "isolated");
expectWouldRemove(dryRun, "no-deps 1.1.0 -> 1.0.0", 4);
expect(dryRun.exitCode).toBe(0);
expect(await lock(packageDir)).toBe(lockBefore);
expect(await readdirSorted(join(packageDir, "node_modules", ".bun"))).toStrictEqual(store);
for (const [flag, reason] of REFUSALS) {
const refused = await dedupe(packageDir, flag, "--linker", "isolated");
expectRefusedToWrite(refused, "no-deps 1.1.0 -> 1.0.0", 4, reason);
expect(await lock(packageDir)).toBe(lockBefore);
}
expect(await readdirSorted(join(packageDir, "node_modules", ".bun"))).toStrictEqual(store);
});