Files
bun.sh/test/cli/install/bun-link.test.ts
Dylan Conway aad4d800ff add "configVersion" to bun.lock(b) (#24236)
### What does this PR do?

Adds `"configVersion"` to bun.lock(b). The version will be used to keep
default settings the same if they would be breaking across bun versions.

fixes ENG-21389
fixes ENG-21388
### How did you verify your code works?
TODO:
- [ ] new project
- [ ] existing project without configVersion
- [ ] existing project with configVersion
- [ ] same as above but with bun.lockb
- [ ] configVersion@0 defaults to hoisted linker
- [ ] new projects use isolated linker

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
2025-11-03 22:20:07 -08:00

475 lines
13 KiB
TypeScript

import { file, spawn } from "bun";
import { afterAll, afterEach, beforeAll, beforeEach, expect, it } from "bun:test";
import { access, mkdir, writeFile } from "fs/promises";
import {
bunExe,
bunEnv as env,
isWindows,
readdirSorted,
runBunInstall,
stderrForInstall,
tmpdirSync,
toBeValidBin,
toHaveBins,
} from "harness";
import { basename, join } from "path";
import { dummyAfterAll, dummyAfterEach, dummyBeforeAll, dummyBeforeEach, package_dir } from "./dummy.registry";
beforeAll(dummyBeforeAll);
afterAll(dummyAfterAll);
let link_dir: string;
expect.extend({
toBeValidBin,
toHaveBins,
});
beforeEach(async () => {
link_dir = tmpdirSync();
await dummyBeforeEach({ linker: "hoisted" });
});
afterEach(async () => {
await dummyAfterEach();
});
it("should link and unlink workspace package", async () => {
await writeFile(
join(link_dir, "package.json"),
JSON.stringify({
name: "foo",
version: "1.0.0",
workspaces: ["packages/*"],
}),
);
await mkdir(join(link_dir, "packages", "moo"), { recursive: true });
await mkdir(join(link_dir, "packages", "boba"), { recursive: true });
await writeFile(
join(link_dir, "packages", "moo", "package.json"),
JSON.stringify({
name: "moo",
version: "0.0.1",
}),
);
await writeFile(
join(link_dir, "packages", "boba", "package.json"),
JSON.stringify({
name: "boba",
version: "0.0.1",
}),
);
let { out, err } = await runBunInstall(env, link_dir);
expect(err.split(/\r?\n/).slice(-2)).toEqual(["Saved lockfile", ""]);
expect(out.replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun install v1."),
"",
"Done! Checked 3 packages (no changes)",
]);
let { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "link"],
cwd: join(link_dir, "packages", "moo"),
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
err = stderrForInstall(await stderr.text());
expect(err.split(/\r?\n/)).toEqual([""]);
expect(await stdout.text()).toContain(`Success! Registered "moo"`);
expect(await exited).toBe(0);
({ stdout, stderr, exited } = spawn({
cmd: [bunExe(), "link", "moo", "--linker=hoisted"],
cwd: join(link_dir, "packages", "boba"),
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
}));
err = stderrForInstall(await stderr.text());
expect(err.split(/\r?\n/)).toEqual([""]);
expect((await stdout.text()).replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun link v1."),
"",
`installed moo@link:moo`,
"",
"1 package installed",
]);
expect(await exited).toBe(0);
expect(await file(join(link_dir, "packages", "boba", "node_modules", "moo", "package.json")).json()).toEqual({
name: "moo",
version: "0.0.1",
});
({ stdout, stderr, exited } = spawn({
cmd: [bunExe(), "unlink"],
cwd: join(link_dir, "packages", "moo"),
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
}));
err = stderrForInstall(await stderr.text());
expect(err.split(/\r?\n/)).toEqual([""]);
expect(await stdout.text()).toContain(`success: unlinked package "moo"`);
expect(await exited).toBe(0);
// link the workspace root package to a workspace package
({ stdout, stderr, exited } = spawn({
cmd: [bunExe(), "link"],
cwd: link_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
}));
err = stderrForInstall(await stderr.text());
expect(err.split(/\r?\n/)).toEqual([""]);
expect(await stdout.text()).toContain(`Success! Registered "foo"`);
expect(await exited).toBe(0);
({ stdout, stderr, exited } = spawn({
cmd: [bunExe(), "link", "foo", "--linker=hoisted"],
cwd: join(link_dir, "packages", "boba"),
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
}));
err = stderrForInstall(await stderr.text());
expect(err.split(/\r?\n/)).toEqual([""]);
expect((await stdout.text()).replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun link v1."),
"",
`installed foo@link:foo`,
"",
"1 package installed",
]);
expect(await file(join(link_dir, "packages", "boba", "node_modules", "foo", "package.json")).json()).toEqual({
name: "foo",
version: "1.0.0",
workspaces: ["packages/*"],
});
expect(await exited).toBe(0);
({ stdout, stderr, exited } = spawn({
cmd: [bunExe(), "unlink"],
cwd: link_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
}));
err = stderrForInstall(await stderr.text());
expect(err.split(/\r?\n/)).toEqual([""]);
expect(await stdout.text()).toContain(`success: unlinked package "foo"`);
expect(await exited).toBe(0);
});
it("should link package", async () => {
const link_name = basename(link_dir).slice("bun-link.".length);
await writeFile(
join(link_dir, "package.json"),
JSON.stringify({
name: link_name,
version: "0.0.1",
}),
);
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({
name: "foo",
version: "0.0.2",
}),
);
const {
stdout: stdout1,
stderr: stderr1,
exited: exited1,
} = spawn({
cmd: [bunExe(), "link"],
cwd: link_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const err1 = stderrForInstall(await new Response(stderr1).text());
expect(err1.split(/\r?\n/)).toEqual([""]);
expect(await new Response(stdout1).text()).toContain(`Success! Registered "${link_name}"`);
expect(await exited1).toBe(0);
const {
stdout: stdout2,
stderr: stderr2,
exited: exited2,
} = spawn({
cmd: [bunExe(), "link", link_name],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const err2 = stderrForInstall(await new Response(stderr2).text());
expect(err2.split(/\r?\n/)).toEqual([""]);
const out2 = await new Response(stdout2).text();
expect(out2.replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun link v1."),
"",
`installed ${link_name}@link:${link_name}`,
"",
"1 package installed",
]);
expect(await exited2).toBe(0);
const {
stdout: stdout3,
stderr: stderr3,
exited: exited3,
} = spawn({
cmd: [bunExe(), "unlink"],
cwd: link_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const err3 = stderrForInstall(await new Response(stderr3).text());
expect(err3.split(/\r?\n/)).toEqual([""]);
expect(await new Response(stdout3).text()).toContain(`success: unlinked package "${link_name}"`);
expect(await exited3).toBe(0);
const {
stdout: stdout4,
stderr: stderr4,
exited: exited4,
} = spawn({
cmd: [bunExe(), "link", link_name],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const err4 = stderrForInstall(await new Response(stderr4).text());
expect(err4).toContain(`error: Package "${link_name}" is not linked`);
expect(await new Response(stdout4).text()).toEqual(expect.stringContaining("bun link v1."));
expect(await exited4).toBe(1);
});
it("should link scoped package", async () => {
const link_name = `@${basename(link_dir).slice("bun-link.".length)}/foo`;
await writeFile(
join(link_dir, "package.json"),
JSON.stringify({
name: link_name,
version: "0.0.1",
}),
);
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({
name: "bar",
version: "0.0.2",
}),
);
const {
stdout: stdout1,
stderr: stderr1,
exited: exited1,
} = spawn({
cmd: [bunExe(), "link"],
cwd: link_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const err1 = stderrForInstall(await new Response(stderr1).text());
expect(err1.split(/\r?\n/)).toEqual([""]);
expect(await new Response(stdout1).text()).toContain(`Success! Registered "${link_name}"`);
expect(await exited1).toBe(0);
const {
stdout: stdout2,
stderr: stderr2,
exited: exited2,
} = spawn({
cmd: [bunExe(), "link", link_name],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const err2 = stderrForInstall(await new Response(stderr2).text());
expect(err2.split(/\r?\n/)).toEqual([""]);
const out2 = await new Response(stdout2).text();
expect(out2.replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun link v1."),
"",
`installed ${link_name}@link:${link_name}`,
"",
"1 package installed",
]);
expect(await exited2).toBe(0);
const {
stdout: stdout3,
stderr: stderr3,
exited: exited3,
} = spawn({
cmd: [bunExe(), "unlink"],
cwd: link_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const err3 = stderrForInstall(await new Response(stderr3).text());
expect(err3.split(/\r?\n/)).toEqual([""]);
expect(await new Response(stdout3).text()).toContain(`success: unlinked package "${link_name}"`);
expect(await exited3).toBe(0);
const {
stdout: stdout4,
stderr: stderr4,
exited: exited4,
} = spawn({
cmd: [bunExe(), "link", link_name],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const err4 = stderrForInstall(await new Response(stderr4).text());
expect(err4).toContain(`error: Package "${link_name}" is not linked`);
expect((await new Response(stdout4).text()).split(/\r?\n/)).toEqual([expect.stringContaining("bun link v1."), ""]);
expect(await exited4).toBe(1);
});
it("should link dependency without crashing", async () => {
const link_name = basename(link_dir).slice("bun-link.".length) + "-really-long-name";
await writeFile(
join(link_dir, "package.json"),
JSON.stringify({
name: link_name,
version: "0.0.1",
bin: {
[link_name]: `${link_name}.py`,
},
}),
);
// Use a Python script with \r\n shebang to test normalization
await writeFile(join(link_dir, `${link_name}.py`), "#!/usr/bin/env python\r\nprint('hello from python')");
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({
name: "foo",
version: "0.0.2",
dependencies: {
[link_name]: `link:${link_name}`,
},
}),
);
const {
stdout: stdout1,
stderr: stderr1,
exited: exited1,
} = spawn({
cmd: [bunExe(), "link"],
cwd: link_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const err1 = stderrForInstall(await new Response(stderr1).text());
expect(err1.split(/\r?\n/)).toEqual([""]);
expect(await new Response(stdout1).text()).toContain(`Success! Registered "${link_name}"`);
expect(await exited1).toBe(0);
const { out: stdout2, err: stderr2, exited: exited2 } = await runBunInstall(env, package_dir);
const err2 = stderrForInstall(await new Response(stderr2).text());
expect(err2.split(/\r?\n/).slice(-2)).toEqual(["Saved lockfile", ""]);
const out2 = await new Response(stdout2).text();
expect(out2.replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun install v1."),
"",
`+ ${link_name}@link:${link_name}`,
"",
"1 package installed",
]);
expect(await exited2).toBe(0);
expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", link_name].sort());
expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins([link_name]);
expect(join(package_dir, "node_modules", ".bin", link_name)).toBeValidBin(join("..", link_name, `${link_name}.py`));
expect(await readdirSorted(join(package_dir, "node_modules", link_name))).toEqual(
["package.json", `${link_name}.py`].sort(),
);
// Verify that the shebang was normalized from \r\n to \n (only on non-Windows)
const binContent = await file(join(package_dir, "node_modules", link_name, `${link_name}.py`)).text();
if (isWindows) {
expect(binContent).toStartWith("#!/usr/bin/env python\r\nprint");
} else {
expect(binContent).toStartWith("#!/usr/bin/env python\nprint");
expect(binContent).not.toContain("\r\n");
}
await access(join(package_dir, "bun.lockb"));
const {
stdout: stdout3,
stderr: stderr3,
exited: exited3,
} = spawn({
cmd: [bunExe(), "unlink"],
cwd: link_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const err3 = stderrForInstall(await new Response(stderr3).text());
expect(err3.split(/\r?\n/)).toEqual([""]);
expect(await new Response(stdout3).text()).toContain(`success: unlinked package "${link_name}"`);
expect(await exited3).toBe(0);
const {
stdout: stdout4,
stderr: stderr4,
exited: exited4,
} = spawn({
cmd: [bunExe(), "install"],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const err4 = stderrForInstall(await new Response(stderr4).text());
expect(err4).toContain(`FileNotFound: failed linking dependency/workspace to node_modules for package ${link_name}`);
const out4 = await new Response(stdout4).text();
expect(out4.replace(/\[[0-9\.]+m?s\]/, "[]").split(/\r?\n/)).toEqual([
expect.stringContaining("bun install v1."),
"",
"Failed to install 1 package",
"[] done",
"",
]);
// This should fail with a non-zero exit code.
expect(await exited4).toBe(1);
});