Compare commits

...

2 Commits

Author SHA1 Message Date
Claude Bot
e0a30961fe fix(test): remove global state dependency in bun-add tests
Remove usage of check_npm_auth_type global state which caused race
conditions in concurrent tests. The test that needed to skip the
npm-auth-type check now uses a custom handler instead of modifying
global state.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-08 21:30:13 +00:00
Claude Bot
b7b1507d52 refactor(test): convert install tests to use withContext pattern
Convert 12 test files from the legacy dummy.registry pattern to the new
withContext pattern for concurrent test execution:

- bun-add.test.ts
- bun-install-cpu-os.test.ts
- bun-install-retry.test.ts
- bun-install-security-provider.test.ts
- bun-link.test.ts
- bun-pm.test.ts
- bun-remove.test.ts
- bun-update-security-provider.test.ts
- bun-update.test.ts
- bunx.test.ts
- lockfile-only.test.ts
- test/regression/issue/08093.test.ts

Changes made in each file:
- Replace dummyBeforeEach/dummyAfterEach with createTestContext/destroyTestContext
- Replace setHandler(dummyRegistry(...)) with setContextHandler(ctx, dummyRegistryForContext(ctx, ...))
- Replace package_dir with ctx.package_dir
- Replace root_url with ctx.registry_url (including trailing slash adjustment)
- Replace requested with ctx.requested
- Wrap tests in describe.concurrent() for parallel execution
- Use withContext() helper to manage test context lifecycle

This allows tests to run concurrently by giving each test its own isolated
registry URL prefix, improving test execution speed significantly.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-08 21:30:13 +00:00
13 changed files with 6114 additions and 5689 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +1,22 @@
import { file, spawn } from "bun";
import { afterAll, afterEach, beforeAll, beforeEach, expect, it, setDefaultTimeout } from "bun:test";
import { afterAll, beforeAll, describe, expect, it, setDefaultTimeout } from "bun:test";
import { access, writeFile } from "fs/promises";
import { bunExe, bunEnv as env, readdirSorted, tmpdirSync, toBeValidBin, toBeWorkspaceLink, toHaveBins } from "harness";
import { join } from "path";
import {
createTestContext,
destroyTestContext,
dummyAfterAll,
dummyAfterEach,
dummyBeforeAll,
dummyBeforeEach,
dummyRegistry,
package_dir,
requested,
root_url,
setHandler,
dummyRegistryForContext,
setContextHandler,
type TestContext,
} from "./dummy.registry";
beforeAll(dummyBeforeAll);
beforeAll(() => {
setDefaultTimeout(1000 * 60 * 5);
dummyBeforeAll();
});
afterAll(dummyAfterAll);
expect.extend({
@@ -24,88 +25,94 @@ expect.extend({
toBeWorkspaceLink,
});
let port: string;
let add_dir: string;
beforeAll(() => {
setDefaultTimeout(1000 * 60 * 5);
port = new URL(root_url).port;
});
async function withContext(
opts: { linker?: "hoisted" | "isolated" } | undefined,
fn: (ctx: TestContext) => Promise<void>,
): Promise<void> {
const ctx = await createTestContext(opts ? { linker: opts.linker! } : undefined);
try {
await fn(ctx);
} finally {
destroyTestContext(ctx);
}
}
beforeEach(async () => {
add_dir = tmpdirSync();
await dummyBeforeEach();
});
afterEach(async () => {
await dummyAfterEach();
});
const defaultOpts = { linker: "hoisted" as const };
it("retries on 500", async () => {
const urls: string[] = [];
setHandler(dummyRegistry(urls, undefined, 4));
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({
name: "foo",
version: "0.0.1",
}),
);
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "add", "BaR", "--linker=hoisted"],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
describe.concurrent("bun-install-retry", () => {
it("retries on 500", async () => {
await withContext(defaultOpts, async ctx => {
const add_dir = tmpdirSync();
const port = new URL(ctx.registry_url).port;
const urls: string[] = [];
setContextHandler(ctx, dummyRegistryForContext(ctx, urls, undefined, 4));
await writeFile(
join(ctx.package_dir, "package.json"),
JSON.stringify({
name: "foo",
version: "0.0.1",
}),
);
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "add", "BaR", "--linker=hoisted"],
cwd: ctx.package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const err = await stderr.text();
expect(err).not.toContain("error:");
expect(err).toContain("Saved lockfile");
const out = await stdout.text();
expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun add v1."),
"",
"installed BaR@0.0.2",
"",
"1 package installed",
]);
expect(await exited).toBe(0);
expect(urls.sort()).toEqual([
`${ctx.registry_url}BaR`,
`${ctx.registry_url}BaR`,
`${ctx.registry_url}BaR`,
`${ctx.registry_url}BaR`,
`${ctx.registry_url}BaR`,
`${ctx.registry_url}BaR`,
`${ctx.registry_url}BaR-0.0.2.tgz`,
`${ctx.registry_url}BaR-0.0.2.tgz`,
`${ctx.registry_url}BaR-0.0.2.tgz`,
`${ctx.registry_url}BaR-0.0.2.tgz`,
`${ctx.registry_url}BaR-0.0.2.tgz`,
`${ctx.registry_url}BaR-0.0.2.tgz`,
]);
expect(ctx.requested).toBe(12);
await Promise.all([
(async () => expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "BaR"]))(),
(async () =>
expect(await readdirSorted(join(ctx.package_dir, "node_modules", "BaR"))).toEqual(["package.json"]))(),
(async () =>
expect(await file(join(ctx.package_dir, "node_modules", "BaR", "package.json")).json()).toEqual({
name: "bar",
version: "0.0.2",
}))(),
(async () =>
expect(await file(join(ctx.package_dir, "package.json")).text()).toEqual(
JSON.stringify(
{
name: "foo",
version: "0.0.1",
dependencies: {
BaR: "^0.0.2",
},
},
null,
2,
),
))(),
async () => await access(join(ctx.package_dir, "bun.lockb")),
]);
});
});
const err = await stderr.text();
expect(err).not.toContain("error:");
expect(err).toContain("Saved lockfile");
const out = await stdout.text();
expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun add v1."),
"",
"installed BaR@0.0.2",
"",
"1 package installed",
]);
expect(await exited).toBe(0);
expect(urls.sort()).toEqual([
`${root_url}/BaR`,
`${root_url}/BaR`,
`${root_url}/BaR`,
`${root_url}/BaR`,
`${root_url}/BaR`,
`${root_url}/BaR`,
`${root_url}/BaR-0.0.2.tgz`,
`${root_url}/BaR-0.0.2.tgz`,
`${root_url}/BaR-0.0.2.tgz`,
`${root_url}/BaR-0.0.2.tgz`,
`${root_url}/BaR-0.0.2.tgz`,
`${root_url}/BaR-0.0.2.tgz`,
]);
expect(requested).toBe(12);
await Promise.all([
(async () => expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "BaR"]))(),
(async () => expect(await readdirSorted(join(package_dir, "node_modules", "BaR"))).toEqual(["package.json"]))(),
(async () =>
expect(await file(join(package_dir, "node_modules", "BaR", "package.json")).json()).toEqual({
name: "bar",
version: "0.0.2",
}))(),
(async () =>
expect(await file(join(package_dir, "package.json")).text()).toEqual(
JSON.stringify(
{
name: "foo",
version: "0.0.1",
dependencies: {
BaR: "^0.0.2",
},
},
null,
2,
),
))(),
async () => await access(join(package_dir, "bun.lockb")),
]);
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
import { file, spawn } from "bun";
import { afterAll, afterEach, beforeAll, beforeEach, expect, it } from "bun:test";
import { afterAll, beforeAll, describe, expect, it, setDefaultTimeout } from "bun:test";
import { access, mkdir, writeFile } from "fs/promises";
import {
bunExe,
@@ -13,462 +13,497 @@ import {
toHaveBins,
} from "harness";
import { basename, join } from "path";
import { dummyAfterAll, dummyAfterEach, dummyBeforeAll, dummyBeforeEach, package_dir } from "./dummy.registry";
import {
createTestContext,
destroyTestContext,
dummyAfterAll,
dummyBeforeAll,
type TestContext,
} from "./dummy.registry";
beforeAll(dummyBeforeAll);
beforeAll(() => {
setDefaultTimeout(1000 * 60 * 5);
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");
async function withContext(
opts: { linker?: "hoisted" | "isolated" } | undefined,
fn: (ctx: TestContext) => Promise<void>,
): Promise<void> {
const ctx = await createTestContext(opts ? { linker: opts.linker! } : undefined);
try {
await fn(ctx);
} finally {
destroyTestContext(ctx);
}
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 defaultOpts = { linker: "hoisted" as const };
describe.concurrent("bun-link", () => {
it("should link and unlink workspace package", async () => {
await withContext(defaultOpts, async ctx => {
const link_dir = tmpdirSync();
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);
});
});
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,
it("should link package", async () => {
await withContext(defaultOpts, async ctx => {
const link_dir = tmpdirSync();
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(ctx.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: ctx.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: ctx.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);
});
});
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);
it("should link scoped package", async () => {
await withContext(defaultOpts, async ctx => {
const link_dir = tmpdirSync();
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(ctx.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: ctx.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: ctx.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 () => {
await withContext(defaultOpts, async ctx => {
const link_dir = tmpdirSync();
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(ctx.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, ctx.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(ctx.package_dir, "node_modules"))).toEqual([".bin", ".cache", link_name].sort());
expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins([link_name]);
expect(join(ctx.package_dir, "node_modules", ".bin", link_name)).toBeValidBin(
join("..", link_name, `${link_name}.py`),
);
expect(await readdirSorted(join(ctx.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(ctx.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(ctx.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: ctx.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);
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,320 +1,347 @@
import { file, spawn } from "bun";
import { afterAll, afterEach, beforeAll, beforeEach, expect, it } from "bun:test";
import { afterAll, beforeAll, describe, expect, it, setDefaultTimeout } from "bun:test";
import { mkdir, writeFile } from "fs/promises";
import { bunExe, bunEnv as env, tmpdirSync } from "harness";
import { join, relative } from "path";
import { dummyAfterAll, dummyAfterEach, dummyBeforeAll, dummyBeforeEach, package_dir } from "./dummy.registry";
import {
createTestContext,
destroyTestContext,
dummyAfterAll,
dummyBeforeAll,
type TestContext,
} from "./dummy.registry";
beforeAll(dummyBeforeAll);
beforeAll(() => {
setDefaultTimeout(1000 * 60 * 5);
dummyBeforeAll();
});
afterAll(dummyAfterAll);
let remove_dir: string;
async function withContext(
opts: { linker?: "hoisted" | "isolated" } | undefined,
fn: (ctx: TestContext) => Promise<void>,
): Promise<void> {
const ctx = await createTestContext(opts ? { linker: opts.linker! } : undefined);
try {
await fn(ctx);
} finally {
destroyTestContext(ctx);
}
}
beforeEach(async () => {
remove_dir = tmpdirSync();
await dummyBeforeEach();
});
const defaultOpts = { linker: "hoisted" as const };
afterEach(async () => {
await dummyAfterEach();
});
describe.concurrent("bun-remove", () => {
it("should remove existing package", async () => {
await withContext(defaultOpts, async ctx => {
const remove_dir = tmpdirSync();
const pkg1_dir = join(remove_dir, "pkg1");
const pkg1_path = relative(ctx.package_dir, pkg1_dir);
await mkdir(pkg1_dir);
const pkg2_dir = join(remove_dir, "pkg2");
const pkg2_path = relative(ctx.package_dir, pkg2_dir);
await mkdir(pkg2_dir);
it("should remove existing package", async () => {
const pkg1_dir = join(remove_dir, "pkg1");
const pkg1_path = relative(package_dir, pkg1_dir);
await mkdir(pkg1_dir);
const pkg2_dir = join(remove_dir, "pkg2");
const pkg2_path = relative(package_dir, pkg2_dir);
await mkdir(pkg2_dir);
await writeFile(
join(pkg1_dir, "package.json"),
JSON.stringify({
name: "pkg1",
version: "0.0.1",
}),
);
await writeFile(
join(pkg2_dir, "package.json"),
JSON.stringify({
name: "pkg2",
version: "0.0.1",
}),
);
await writeFile(
join(ctx.package_dir, "package.json"),
JSON.stringify({
name: "foo",
version: "0.0.2",
}),
);
const { exited: exited1 } = spawn({
cmd: [bunExe(), "add", `file:${pkg1_path}`.replace(/\\/g, "\\\\")],
cwd: ctx.package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
expect(await exited1).toBe(0);
const { exited: exited2 } = spawn({
cmd: [bunExe(), "add", `file:${pkg2_path}`.replace(/\\/g, "\\\\")],
cwd: ctx.package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
expect(await exited2).toBe(0);
expect(await file(join(ctx.package_dir, "package.json")).text()).toEqual(
JSON.stringify(
{
name: "foo",
version: "0.0.2",
dependencies: {
pkg1: `file:${pkg1_path.replace(/\\/g, "/")}`,
pkg2: `file:${pkg2_path.replace(/\\/g, "/")}`,
},
},
null,
2,
),
);
await writeFile(
join(pkg1_dir, "package.json"),
JSON.stringify({
name: "pkg1",
version: "0.0.1",
}),
);
await writeFile(
join(pkg2_dir, "package.json"),
JSON.stringify({
name: "pkg2",
version: "0.0.1",
}),
);
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({
name: "foo",
version: "0.0.2",
}),
);
const { exited: exited1 } = spawn({
cmd: [bunExe(), "add", `file:${pkg1_path}`.replace(/\\/g, "\\\\")],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
const {
exited: removeExited1,
stdout: stdout1,
stderr: stderr1,
} = spawn({
cmd: [bunExe(), "remove", "pkg1"],
cwd: ctx.package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const out1 = await new Response(stdout1).text();
const err1 = await new Response(stderr1).text();
expect(out1.replace(/\s*\[[0-9\.]+m?s\]/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun remove v1."),
"",
`+ pkg2@${pkg2_path.replace(/\\/g, "/")}`,
"",
"1 package installed",
"Removed: 1",
"",
]);
expect(err1.split(/\r?\n/)).toEqual(["Saved lockfile", ""]);
expect(await removeExited1).toBe(0);
expect(await file(join(ctx.package_dir, "package.json")).text()).toEqual(
JSON.stringify(
{
name: "foo",
version: "0.0.2",
dependencies: {
pkg2: `file:${pkg2_path.replace(/\\/g, "/")}`,
},
},
null,
2,
),
);
const {
exited: removeExited2,
stdout: stdout2,
stderr: stderr2,
} = spawn({
cmd: [bunExe(), "remove", "pkg2"],
cwd: ctx.package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const out2 = await new Response(stdout2).text();
const err2 = await new Response(stderr2).text();
expect(out2.replace(/ \[[0-9\.]+m?s\]/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun remove v1."),
"",
"- pkg2",
"1 package removed",
"",
]);
expect(err2.split(/\r?\n/)).toEqual(["", "package.json has no dependencies! Deleted empty lockfile", ""]);
expect(await removeExited2).toBe(0);
expect(await file(join(ctx.package_dir, "package.json")).text()).toEqual(
JSON.stringify(
{
name: "foo",
version: "0.0.2",
},
null,
2,
),
);
});
});
expect(await exited1).toBe(0);
const { exited: exited2 } = spawn({
cmd: [bunExe(), "add", `file:${pkg2_path}`.replace(/\\/g, "\\\\")],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
it("should not reject missing package", async () => {
await withContext(defaultOpts, async ctx => {
const remove_dir = tmpdirSync();
await writeFile(
join(ctx.package_dir, "package.json"),
JSON.stringify({
name: "foo",
version: "0.0.1",
}),
);
await writeFile(
join(remove_dir, "package.json"),
JSON.stringify({
name: "pkg1",
version: "0.0.2",
}),
);
const pkg_path = relative(ctx.package_dir, remove_dir);
const { exited: addExited } = spawn({
cmd: [bunExe(), "add", `file:${pkg_path}`.replace(/\\/g, "\\\\")],
cwd: ctx.package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
expect(await addExited).toBe(0);
const { exited: rmExited } = spawn({
cmd: [bunExe(), "remove", "pkg2"],
cwd: ctx.package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
expect(await rmExited).toBe(0);
});
});
expect(await exited2).toBe(0);
expect(await file(join(package_dir, "package.json")).text()).toEqual(
JSON.stringify(
{
it("should not affect if package is not installed", async () => {
await withContext(defaultOpts, async ctx => {
await writeFile(
join(ctx.package_dir, "package.json"),
JSON.stringify({
name: "foo",
version: "0.0.1",
}),
);
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "remove", "pkg"],
cwd: ctx.package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const out = await stdout.text();
expect(out.split("\n")).toEqual([expect.stringContaining("bun remove v1."), ""]);
const err = await stderr.text();
expect(err.replace(/ \[[0-9\.]+m?s\]/, "").split(/\r?\n/)).toEqual([
"package.json doesn't have dependencies, there's nothing to remove!",
"",
]);
expect(await exited).toBe(0);
});
});
it("should retain a new line in the end of package.json", async () => {
await withContext(defaultOpts, async ctx => {
const remove_dir = tmpdirSync();
await writeFile(
join(ctx.package_dir, "package.json"),
JSON.stringify({
name: "foo",
version: "0.0.1",
}),
);
await writeFile(
join(remove_dir, "package.json"),
JSON.stringify({
name: "pkg",
version: "0.0.2",
}),
);
const pkg_path = relative(ctx.package_dir, remove_dir);
const { exited: addExited } = spawn({
cmd: [bunExe(), "add", `file:${pkg_path}`.replace(/\\/g, "\\\\")],
cwd: ctx.package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
expect(await addExited).toBe(0);
const content_before_remove = await file(join(ctx.package_dir, "package.json")).text();
expect(content_before_remove.endsWith("}")).toBe(true);
expect(content_before_remove).toEqual(
JSON.stringify(
{
name: "foo",
version: "0.0.1",
dependencies: {
pkg: `file:${pkg_path.replace(/\\/g, "/")}`,
},
},
null,
2,
),
);
await writeFile(join(ctx.package_dir, "package.json"), content_before_remove + "\n");
const { exited } = spawn({
cmd: [bunExe(), "remove", "pkg"],
cwd: ctx.package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
expect(await exited).toBe(0);
const content_after_remove = await file(join(ctx.package_dir, "package.json")).text();
expect(content_after_remove.endsWith("}\n")).toBe(true);
expect(content_after_remove).toEqual(
JSON.stringify(
{
name: "foo",
version: "0.0.1",
},
null,
2,
) + "\n",
);
});
});
it("should remove peerDependencies", async () => {
await withContext(defaultOpts, async ctx => {
await writeFile(
join(ctx.package_dir, "package.json"),
JSON.stringify({
name: "foo",
peerDependencies: {
bar: "~0.0.1",
},
}),
);
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "remove", "bar"],
cwd: ctx.package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const err = await stderr.text();
expect(err).not.toContain("error:");
const out = await stdout.text();
expect(out.replace(/\[[0-9\.]+m?s\]/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun remove v1."),
"",
" done",
"",
]);
expect(await exited).toBe(0);
expect(await file(join(ctx.package_dir, "package.json")).json()).toEqual({
name: "foo",
version: "0.0.2",
dependencies: {
pkg1: `file:${pkg1_path.replace(/\\/g, "/")}`,
pkg2: `file:${pkg2_path.replace(/\\/g, "/")}`,
},
},
null,
2,
),
);
const {
exited: removeExited1,
stdout: stdout1,
stderr: stderr1,
} = spawn({
cmd: [bunExe(), "remove", "pkg1"],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
expect(await removeExited1).toBe(0);
const out1 = await new Response(stdout1).text();
const err1 = await new Response(stderr1).text();
expect(out1.replace(/\s*\[[0-9\.]+m?s\]/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun remove v1."),
"",
`+ pkg2@${pkg2_path.replace(/\\/g, "/")}`,
"",
"1 package installed",
"Removed: 1",
"",
]);
expect(err1.split(/\r?\n/)).toEqual(["Saved lockfile", ""]);
expect(await file(join(package_dir, "package.json")).text()).toEqual(
JSON.stringify(
{
name: "foo",
version: "0.0.2",
dependencies: {
pkg2: `file:${pkg2_path.replace(/\\/g, "/")}`,
},
},
null,
2,
),
);
const {
exited: removeExited2,
stdout: stdout2,
stderr: stderr2,
} = spawn({
cmd: [bunExe(), "remove", "pkg2"],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
expect(await removeExited2).toBe(0);
const out2 = await new Response(stdout2).text();
const err2 = await new Response(stderr2).text();
expect(out2.replace(/ \[[0-9\.]+m?s\]/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun remove v1."),
"",
"- pkg2",
"1 package removed",
"",
]);
expect(err2.split(/\r?\n/)).toEqual(["", "package.json has no dependencies! Deleted empty lockfile", ""]);
expect(await file(join(package_dir, "package.json")).text()).toEqual(
JSON.stringify(
{
name: "foo",
version: "0.0.2",
},
null,
2,
),
);
});
it("should not reject missing package", async () => {
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({
name: "foo",
version: "0.0.1",
}),
);
await writeFile(
join(remove_dir, "package.json"),
JSON.stringify({
name: "pkg1",
version: "0.0.2",
}),
);
const pkg_path = relative(package_dir, remove_dir);
const { exited: addExited } = spawn({
cmd: [bunExe(), "add", `file:${pkg_path}`.replace(/\\/g, "\\\\")],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
expect(await addExited).toBe(0);
const { exited: rmExited } = spawn({
cmd: [bunExe(), "remove", "pkg2"],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
expect(await rmExited).toBe(0);
});
it("should not affect if package is not installed", async () => {
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({
name: "foo",
version: "0.0.1",
}),
);
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "remove", "pkg"],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
expect(await exited).toBe(0);
const out = await stdout.text();
expect(out.split("\n")).toEqual([expect.stringContaining("bun remove v1."), ""]);
const err = await stderr.text();
expect(err.replace(/ \[[0-9\.]+m?s\]/, "").split(/\r?\n/)).toEqual([
"package.json doesn't have dependencies, there's nothing to remove!",
"",
]);
});
it("should retain a new line in the end of package.json", async () => {
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({
name: "foo",
version: "0.0.1",
}),
);
await writeFile(
join(remove_dir, "package.json"),
JSON.stringify({
name: "pkg",
version: "0.0.2",
}),
);
const pkg_path = relative(package_dir, remove_dir);
const { exited: addExited } = spawn({
cmd: [bunExe(), "add", `file:${pkg_path}`.replace(/\\/g, "\\\\")],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
expect(await addExited).toBe(0);
const content_before_remove = await file(join(package_dir, "package.json")).text();
expect(content_before_remove.endsWith("}")).toBe(true);
expect(content_before_remove).toEqual(
JSON.stringify(
{
name: "foo",
version: "0.0.1",
dependencies: {
pkg: `file:${pkg_path.replace(/\\/g, "/")}`,
},
},
null,
2,
),
);
await writeFile(join(package_dir, "package.json"), content_before_remove + "\n");
const { exited } = spawn({
cmd: [bunExe(), "remove", "pkg"],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
expect(await exited).toBe(0);
const content_after_remove = await file(join(package_dir, "package.json")).text();
expect(content_after_remove.endsWith("}\n")).toBe(true);
expect(content_after_remove).toEqual(
JSON.stringify(
{
name: "foo",
version: "0.0.1",
},
null,
2,
) + "\n",
);
});
it("should remove peerDependencies", async () => {
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({
name: "foo",
peerDependencies: {
bar: "~0.0.1",
},
}),
);
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "remove", "bar"],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const err = await stderr.text();
expect(err).not.toContain("error:");
const out = await stdout.text();
expect(out.replace(/\[[0-9\.]+m?s\]/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun remove v1."),
"",
" done",
"",
]);
expect(await exited).toBe(0);
expect(await file(join(package_dir, "package.json")).json()).toEqual({
name: "foo",
});
});
});
});

View File

@@ -1,33 +1,54 @@
import { afterAll, afterEach, beforeAll, beforeEach, expect, test } from "bun:test";
import { afterAll, beforeAll, describe, expect, setDefaultTimeout, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
import { join } from "path";
import {
createTestContext,
destroyTestContext,
dummyAfterAll,
dummyAfterEach,
dummyBeforeAll,
dummyBeforeEach,
dummyRegistry,
package_dir,
setHandler,
write,
} from "./dummy.registry.js";
dummyRegistryForContext,
setContextHandler,
type TestContext,
} from "./dummy.registry";
beforeAll(dummyBeforeAll);
afterAll(dummyAfterAll);
beforeEach(async () => {
await dummyBeforeEach();
beforeAll(() => {
setDefaultTimeout(1000 * 60 * 5);
dummyBeforeAll();
});
afterEach(dummyAfterEach);
afterAll(dummyAfterAll);
test("security scanner blocks bun update on fatal advisory", async () => {
const urls: string[] = [];
setHandler(
dummyRegistry(urls, {
"0.1.0": {},
"0.2.0": {},
}),
);
async function withContext(
opts: { linker?: "hoisted" | "isolated" } | undefined,
fn: (ctx: TestContext) => Promise<void>,
): Promise<void> {
const ctx = await createTestContext(opts ? { linker: opts.linker! } : undefined);
try {
await fn(ctx);
} finally {
destroyTestContext(ctx);
}
}
const scannerCode = `
const defaultOpts = { linker: "hoisted" as const };
// Helper function to write to package_dir
async function write(ctx: TestContext, path: string, content: string | object) {
await Bun.write(join(ctx.package_dir, path), typeof content === "string" ? content : JSON.stringify(content));
}
describe.concurrent("Security Scanner for bun update", () => {
test("security scanner blocks bun update on fatal advisory", async () => {
await withContext(defaultOpts, async ctx => {
const urls: string[] = [];
setContextHandler(
ctx,
dummyRegistryForContext(ctx, urls, {
"0.1.0": {},
"0.2.0": {},
}),
);
const scannerCode = `
export const scanner = {
version: "1",
scan: async ({ packages }) => {
@@ -44,109 +65,115 @@ test("security scanner blocks bun update on fatal advisory", async () => {
};
`;
await write("./scanner.ts", scannerCode);
await write("package.json", {
name: "my-app",
version: "1.0.0",
dependencies: {
moo: "0.1.0",
},
});
await write(ctx, "./scanner.ts", scannerCode);
await write(ctx, "package.json", {
name: "my-app",
version: "1.0.0",
dependencies: {
moo: "0.1.0",
},
});
// First install without security scanning (to have something to update)
await using installProc = Bun.spawn({
cmd: [bunExe(), "install", "--no-summary"],
env: bunEnv,
cwd: package_dir,
stdout: "pipe",
stderr: "pipe",
});
// First install without security scanning (to have something to update)
await using installProc = Bun.spawn({
cmd: [bunExe(), "install", "--no-summary"],
env: bunEnv,
cwd: ctx.package_dir,
stdout: "pipe",
stderr: "pipe",
});
await installProc.stdout.text();
await installProc.stderr.text();
await installProc.exited;
await installProc.stdout.text();
await installProc.stderr.text();
await installProc.exited;
await write(
"./bunfig.toml",
`
await write(
ctx,
"./bunfig.toml",
`
[install]
saveTextLockfile = false
[install.security]
scanner = "./scanner.ts"
`,
);
);
await using updateProc = Bun.spawn({
cmd: [bunExe(), "update", "moo"],
env: bunEnv,
cwd: package_dir,
stdout: "pipe",
stderr: "pipe",
await using updateProc = Bun.spawn({
cmd: [bunExe(), "update", "moo"],
env: bunEnv,
cwd: ctx.package_dir,
stdout: "pipe",
stderr: "pipe",
});
const [updateOut, updateErr, updateExitCode] = await Promise.all([
updateProc.stdout.text(),
updateProc.stderr.text(),
updateProc.exited,
]);
expect(updateOut).toContain("FATAL: moo");
expect(updateOut).toContain("Fatal security issue detected");
expect(updateOut).toContain("Installation aborted due to fatal security advisories");
expect(updateExitCode).toBe(1);
});
});
const [updateOut, updateErr, updateExitCode] = await Promise.all([
updateProc.stdout.text(),
updateProc.stderr.text(),
updateProc.exited,
]);
test("security scanner does not run on bun update when disabled", async () => {
await withContext(defaultOpts, async ctx => {
const urls: string[] = [];
setContextHandler(
ctx,
dummyRegistryForContext(ctx, urls, {
"0.1.0": {},
"0.2.0": {},
}),
);
expect(updateOut).toContain("FATAL: moo");
expect(updateOut).toContain("Fatal security issue detected");
expect(updateOut).toContain("Installation aborted due to fatal security advisories");
await write(ctx, "package.json", {
name: "my-app",
version: "1.0.0",
dependencies: {
moo: "0.1.0",
},
});
expect(updateExitCode).toBe(1);
});
test("security scanner does not run on bun update when disabled", async () => {
const urls: string[] = [];
setHandler(
dummyRegistry(urls, {
"0.1.0": {},
"0.2.0": {},
}),
);
await write("package.json", {
name: "my-app",
version: "1.0.0",
dependencies: {
moo: "0.1.0",
},
});
// Remove bunfig.toml to ensure no security scanner
await write("bunfig.toml", "");
await using installProc = Bun.spawn({
cmd: [bunExe(), "install", "--no-summary"],
env: bunEnv,
cwd: package_dir,
stdout: "pipe",
stderr: "pipe",
});
await installProc.stdout.text();
await installProc.stderr.text();
await installProc.exited;
await using updateProc = Bun.spawn({
cmd: [bunExe(), "update", "moo"],
env: bunEnv,
cwd: package_dir,
stdout: "pipe",
stderr: "pipe",
});
const [updateOut, updateErr, updateExitCode] = await Promise.all([
updateProc.stdout.text(),
updateProc.stderr.text(),
updateProc.exited,
]);
expect(updateOut).not.toContain("Security scanner");
expect(updateOut).not.toContain("WARN:");
expect(updateOut).not.toContain("FATAL:");
expect(updateExitCode).toBe(0);
// Remove bunfig.toml to ensure no security scanner
await write(ctx, "bunfig.toml", "");
await using installProc = Bun.spawn({
cmd: [bunExe(), "install", "--no-summary"],
env: bunEnv,
cwd: ctx.package_dir,
stdout: "pipe",
stderr: "pipe",
});
await installProc.stdout.text();
await installProc.stderr.text();
await installProc.exited;
await using updateProc = Bun.spawn({
cmd: [bunExe(), "update", "moo"],
env: bunEnv,
cwd: ctx.package_dir,
stdout: "pipe",
stderr: "pipe",
});
const [updateOut, updateErr, updateExitCode] = await Promise.all([
updateProc.stdout.text(),
updateProc.stderr.text(),
updateProc.exited,
]);
expect(updateOut).not.toContain("Security scanner");
expect(updateOut).not.toContain("WARN:");
expect(updateOut).not.toContain("FATAL:");
expect(updateExitCode).toBe(0);
});
});
});

View File

@@ -1,484 +1,514 @@
import { file, spawn } from "bun";
import { afterAll, afterEach, beforeAll, beforeEach, expect, it } from "bun:test";
import { afterAll, beforeAll, describe, expect, it, setDefaultTimeout } from "bun:test";
import { access, mkdir, readFile, rm, writeFile } from "fs/promises";
import { bunExe, bunEnv as env, readdirSorted, toBeValidBin, toHaveBins } from "harness";
import { join } from "path";
import {
createTestContext,
destroyTestContext,
dummyAfterAll,
dummyAfterEach,
dummyBeforeAll,
dummyBeforeEach,
dummyRegistry,
package_dir,
requested,
root_url,
setHandler,
} from "./dummy.registry.js";
dummyRegistryForContext,
setContextHandler,
type TestContext,
} from "./dummy.registry";
beforeAll(dummyBeforeAll);
afterAll(dummyAfterAll);
beforeEach(async () => {
await dummyBeforeEach();
beforeAll(() => {
setDefaultTimeout(1000 * 60 * 5);
dummyBeforeAll();
});
afterEach(dummyAfterEach);
afterAll(dummyAfterAll);
expect.extend({
toBeValidBin,
toHaveBins,
});
for (const { input } of [{ input: { baz: "~0.0.3", moo: "~0.1.0" } }]) {
it(`should update to latest version of dependency (${input.baz[0]})`, async () => {
const urls: string[] = [];
const tilde = input.baz[0] === "~";
const registry = {
"0.0.3": {
bin: {
"baz-run": "index.js",
},
},
"0.0.5": {
bin: {
"baz-exec": "index.js",
},
},
latest: "0.0.3",
};
setHandler(dummyRegistry(urls, registry));
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({
name: "foo",
dependencies: {
baz: input.baz,
},
}),
);
const {
stdout: stdout1,
stderr: stderr1,
exited: exited1,
} = spawn({
cmd: [bunExe(), "install", "--linker=hoisted"],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const err1 = await new Response(stderr1).text();
expect(err1).not.toContain("error:");
expect(err1).toContain("Saved lockfile");
const out1 = await new Response(stdout1).text();
expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun install v1."),
"",
"+ baz@0.0.3",
"",
"1 package installed",
]);
expect(await exited1).toBe(0);
expect(urls.sort()).toEqual([`${root_url}/baz`, `${root_url}/baz-0.0.3.tgz`]);
expect(requested).toBe(2);
expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "baz"]);
expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]);
expect(join(package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "baz", "index.js"));
expect(await readdirSorted(join(package_dir, "node_modules", "baz"))).toEqual(["index.js", "package.json"]);
expect(await file(join(package_dir, "node_modules", "baz", "package.json")).json()).toEqual({
name: "baz",
version: "0.0.3",
bin: {
"baz-run": "index.js",
},
});
await access(join(package_dir, "bun.lockb"));
// Perform `bun update` with updated registry & lockfile from before
await rm(join(package_dir, "node_modules"), { force: true, recursive: true });
urls.length = 0;
registry.latest = "0.0.5";
setHandler(dummyRegistry(urls, registry));
const {
stdout: stdout2,
stderr: stderr2,
exited: exited2,
} = spawn({
cmd: [bunExe(), "update", "baz", "--linker=hoisted"],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const err2 = await new Response(stderr2).text();
expect(err2).not.toContain("error:");
expect(err2).toContain("Saved lockfile");
const out2 = await new Response(stdout2).text();
expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun update v1."),
"",
`installed baz@${tilde ? "0.0.5" : "0.0.3"} with binaries:`,
` - ${tilde ? "baz-exec" : "baz-run"}`,
"",
"1 package installed",
]);
expect(await exited2).toBe(0);
expect(urls.sort()).toEqual([`${root_url}/baz`, `${root_url}/baz-${tilde ? "0.0.5" : "0.0.3"}.tgz`]);
expect(requested).toBe(4);
expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "baz"]);
expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins([tilde ? "baz-exec" : "baz-run"]);
expect(join(package_dir, "node_modules", ".bin", tilde ? "baz-exec" : "baz-run")).toBeValidBin(
join("..", "baz", "index.js"),
);
expect(await readdirSorted(join(package_dir, "node_modules", "baz"))).toEqual(["index.js", "package.json"]);
expect(await file(join(package_dir, "node_modules", "baz", "package.json")).json()).toEqual({
name: "baz",
version: tilde ? "0.0.5" : "0.0.3",
bin: {
[tilde ? "baz-exec" : "baz-run"]: "index.js",
},
});
expect(await file(join(package_dir, "package.json")).json()).toEqual({
name: "foo",
dependencies: {
baz: tilde ? "~0.0.5" : "^0.0.3",
},
});
await access(join(package_dir, "bun.lockb"));
});
it(`should update to latest versions of dependencies (${input.baz[0]})`, async () => {
const tilde = input.baz[0] === "~";
const urls: string[] = [];
const registry = {
"0.0.3": {
bin: {
"baz-run": "index.js",
},
},
"0.0.5": {
bin: {
"baz-exec": "index.js",
},
},
"0.1.0": {},
latest: "0.0.3",
};
setHandler(dummyRegistry(urls, registry));
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({
name: "foo",
dependencies: {
"@barn/moo": input.moo,
baz: input.baz,
},
}),
);
const {
stdout: stdout1,
stderr: stderr1,
exited: exited1,
} = spawn({
cmd: [bunExe(), "install", "--linker=hoisted"],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const err1 = await new Response(stderr1).text();
expect(err1).not.toContain("error:");
expect(err1).toContain("Saved lockfile");
const out1 = await new Response(stdout1).text();
expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun install v1."),
"",
"+ @barn/moo@0.1.0",
expect.stringContaining("+ baz@0.0.3"),
"",
"2 packages installed",
]);
expect(await exited1).toBe(0);
expect(urls.sort()).toEqual([
`${root_url}/@barn%2fmoo`,
`${root_url}/@barn/moo-0.1.0.tgz`,
`${root_url}/baz`,
`${root_url}/baz-0.0.3.tgz`,
]);
expect(requested).toBe(4);
expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "@barn", "baz"]);
expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]);
expect(join(package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "baz", "index.js"));
expect(await readdirSorted(join(package_dir, "node_modules", "@barn"))).toEqual(["moo"]);
expect(await readdirSorted(join(package_dir, "node_modules", "@barn", "moo"))).toEqual(["package.json"]);
expect(await readdirSorted(join(package_dir, "node_modules", "baz"))).toEqual(["index.js", "package.json"]);
expect(await file(join(package_dir, "node_modules", "baz", "package.json")).json()).toEqual({
name: "baz",
version: "0.0.3",
bin: {
"baz-run": "index.js",
},
});
await access(join(package_dir, "bun.lockb"));
// Perform `bun update` with updated registry & lockfile from before
await rm(join(package_dir, "node_modules"), { force: true, recursive: true });
urls.length = 0;
registry.latest = "0.0.5";
setHandler(dummyRegistry(urls, registry));
const {
stdout: stdout2,
stderr: stderr2,
exited: exited2,
} = spawn({
cmd: [bunExe(), "update", "--linker=hoisted"],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const err2 = await new Response(stderr2).text();
expect(err2).not.toContain("error:");
expect(err2).toContain("Saved lockfile");
const out2 = await new Response(stdout2).text();
if (tilde) {
expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun update v1."),
"",
"^ baz 0.0.3 -> 0.0.5",
"",
"+ @barn/moo@0.1.0",
"",
"2 packages installed",
]);
} else {
expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun update v1."),
"",
expect.stringContaining("+ @barn/moo@0.1.0"),
expect.stringContaining("+ baz@0.0.3"),
"",
"2 packages installed",
]);
}
expect(await exited2).toBe(0);
expect(urls.sort()).toEqual([
`${root_url}/@barn%2fmoo`,
`${root_url}/@barn/moo-0.1.0.tgz`,
`${root_url}/baz`,
tilde ? `${root_url}/baz-0.0.5.tgz` : `${root_url}/baz-0.0.3.tgz`,
]);
expect(requested).toBe(8);
expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "@barn", "baz"]);
expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins([tilde ? "baz-exec" : "baz-run"]);
expect(join(package_dir, "node_modules", ".bin", tilde ? "baz-exec" : "baz-run")).toBeValidBin(
join("..", "baz", "index.js"),
);
expect(await readdirSorted(join(package_dir, "node_modules", "@barn"))).toEqual(["moo"]);
expect(await readdirSorted(join(package_dir, "node_modules", "@barn", "moo"))).toEqual(["package.json"]);
expect(await readdirSorted(join(package_dir, "node_modules", "baz"))).toEqual(["index.js", "package.json"]);
expect(await file(join(package_dir, "node_modules", "baz", "package.json")).json()).toEqual({
name: "baz",
version: tilde ? "0.0.5" : "0.0.3",
bin: {
[tilde ? "baz-exec" : "baz-run"]: "index.js",
},
});
expect(await file(join(package_dir, "package.json")).json()).toEqual({
name: "foo",
dependencies: {
"@barn/moo": tilde ? "~0.1.0" : "^0.1.0",
baz: tilde ? "~0.0.5" : "^0.0.3",
},
});
await access(join(package_dir, "bun.lockb"));
});
async function withContext(
opts: { linker?: "hoisted" | "isolated" } | undefined,
fn: (ctx: TestContext) => Promise<void>,
): Promise<void> {
const ctx = await createTestContext(opts ? { linker: opts.linker! } : undefined);
try {
await fn(ctx);
} finally {
destroyTestContext(ctx);
}
}
it("lockfile should not be modified when there are no version changes, issue#5888", async () => {
// Install packages
const urls: string[] = [];
const registry = {
"0.0.3": {
bin: {
"baz-run": "index.js",
},
},
latest: "0.0.3",
};
setHandler(dummyRegistry(urls, registry));
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({
name: "foo",
dependencies: {
baz: "0.0.3",
},
}),
);
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install", "--linker=hoisted"],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
expect(await exited).toBe(0);
const err1 = await stderr.text();
expect(err1).not.toContain("error:");
expect(err1).toContain("Saved lockfile");
const out1 = await stdout.text();
expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun install v1."),
"",
"+ baz@0.0.3",
"",
"1 package installed",
]);
const defaultOpts = { linker: "hoisted" as const };
// Test if the lockb has been modified by `bun update`.
const getLockbContent = async () => {
const { exited } = spawn({
cmd: [bunExe(), "update"],
cwd: package_dir, // package.json is not changed
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
describe.concurrent("bun-update", () => {
for (const { input } of [{ input: { baz: "~0.0.3", moo: "~0.1.0" } }]) {
it(`should update to latest version of dependency (${input.baz[0]})`, async () => {
await withContext(defaultOpts, async ctx => {
const urls: string[] = [];
const tilde = input.baz[0] === "~";
const registry = {
"0.0.3": {
bin: {
"baz-run": "index.js",
},
},
"0.0.5": {
bin: {
"baz-exec": "index.js",
},
},
latest: "0.0.3",
};
setContextHandler(ctx, dummyRegistryForContext(ctx, urls, registry));
await writeFile(
join(ctx.package_dir, "package.json"),
JSON.stringify({
name: "foo",
dependencies: {
baz: input.baz,
},
}),
);
const {
stdout: stdout1,
stderr: stderr1,
exited: exited1,
} = spawn({
cmd: [bunExe(), "install", "--linker=hoisted"],
cwd: ctx.package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const err1 = await new Response(stderr1).text();
expect(err1).not.toContain("error:");
expect(err1).toContain("Saved lockfile");
const out1 = await new Response(stdout1).text();
expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun install v1."),
"",
"+ baz@0.0.3",
"",
"1 package installed",
]);
expect(await exited1).toBe(0);
expect(urls.sort()).toEqual([`${ctx.registry_url}baz`, `${ctx.registry_url}baz-0.0.3.tgz`]);
expect(ctx.requested).toBe(2);
expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".bin", ".cache", "baz"]);
expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]);
expect(join(ctx.package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "baz", "index.js"));
expect(await readdirSorted(join(ctx.package_dir, "node_modules", "baz"))).toEqual(["index.js", "package.json"]);
expect(await file(join(ctx.package_dir, "node_modules", "baz", "package.json")).json()).toEqual({
name: "baz",
version: "0.0.3",
bin: {
"baz-run": "index.js",
},
});
await access(join(ctx.package_dir, "bun.lockb"));
// Perform `bun update` with updated registry & lockfile from before
await rm(join(ctx.package_dir, "node_modules"), { force: true, recursive: true });
urls.length = 0;
registry.latest = "0.0.5";
setContextHandler(ctx, dummyRegistryForContext(ctx, urls, registry));
const {
stdout: stdout2,
stderr: stderr2,
exited: exited2,
} = spawn({
cmd: [bunExe(), "update", "baz", "--linker=hoisted"],
cwd: ctx.package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const err2 = await new Response(stderr2).text();
expect(err2).not.toContain("error:");
expect(err2).toContain("Saved lockfile");
const out2 = await new Response(stdout2).text();
expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun update v1."),
"",
`installed baz@${tilde ? "0.0.5" : "0.0.3"} with binaries:`,
` - ${tilde ? "baz-exec" : "baz-run"}`,
"",
"1 package installed",
]);
expect(await exited2).toBe(0);
expect(urls.sort()).toEqual([
`${ctx.registry_url}baz`,
`${ctx.registry_url}baz-${tilde ? "0.0.5" : "0.0.3"}.tgz`,
]);
expect(ctx.requested).toBe(4);
expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".bin", ".cache", "baz"]);
expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins([
tilde ? "baz-exec" : "baz-run",
]);
expect(join(ctx.package_dir, "node_modules", ".bin", tilde ? "baz-exec" : "baz-run")).toBeValidBin(
join("..", "baz", "index.js"),
);
expect(await readdirSorted(join(ctx.package_dir, "node_modules", "baz"))).toEqual(["index.js", "package.json"]);
expect(await file(join(ctx.package_dir, "node_modules", "baz", "package.json")).json()).toEqual({
name: "baz",
version: tilde ? "0.0.5" : "0.0.3",
bin: {
[tilde ? "baz-exec" : "baz-run"]: "index.js",
},
});
expect(await file(join(ctx.package_dir, "package.json")).json()).toEqual({
name: "foo",
dependencies: {
baz: tilde ? "~0.0.5" : "^0.0.3",
},
});
await access(join(ctx.package_dir, "bun.lockb"));
});
});
expect(await exited).toBe(0);
return await readFile(join(package_dir, "bun.lockb"));
};
// no changes
expect(await file(join(package_dir, "package.json")).json()).toEqual({
name: "foo",
dependencies: {
baz: "0.0.3",
},
});
let prev = await getLockbContent();
urls.length = 0;
const count = 5;
for (let i = 0; i < count; i++) {
const content = await getLockbContent();
expect(prev).toStrictEqual(content);
prev = content;
it(`should update to latest versions of dependencies (${input.baz[0]})`, async () => {
await withContext(defaultOpts, async ctx => {
const tilde = input.baz[0] === "~";
const urls: string[] = [];
const registry = {
"0.0.3": {
bin: {
"baz-run": "index.js",
},
},
"0.0.5": {
bin: {
"baz-exec": "index.js",
},
},
"0.1.0": {},
latest: "0.0.3",
};
setContextHandler(ctx, dummyRegistryForContext(ctx, urls, registry));
await writeFile(
join(ctx.package_dir, "package.json"),
JSON.stringify({
name: "foo",
dependencies: {
"@barn/moo": input.moo,
baz: input.baz,
},
}),
);
const {
stdout: stdout1,
stderr: stderr1,
exited: exited1,
} = spawn({
cmd: [bunExe(), "install", "--linker=hoisted"],
cwd: ctx.package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const err1 = await new Response(stderr1).text();
expect(err1).not.toContain("error:");
expect(err1).toContain("Saved lockfile");
const out1 = await new Response(stdout1).text();
expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun install v1."),
"",
"+ @barn/moo@0.1.0",
expect.stringContaining("+ baz@0.0.3"),
"",
"2 packages installed",
]);
expect(await exited1).toBe(0);
expect(urls.sort()).toEqual([
`${ctx.registry_url}@barn%2fmoo`,
`${ctx.registry_url}@barn/moo-0.1.0.tgz`,
`${ctx.registry_url}baz`,
`${ctx.registry_url}baz-0.0.3.tgz`,
]);
expect(ctx.requested).toBe(4);
expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".bin", ".cache", "@barn", "baz"]);
expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]);
expect(join(ctx.package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "baz", "index.js"));
expect(await readdirSorted(join(ctx.package_dir, "node_modules", "@barn"))).toEqual(["moo"]);
expect(await readdirSorted(join(ctx.package_dir, "node_modules", "@barn", "moo"))).toEqual(["package.json"]);
expect(await readdirSorted(join(ctx.package_dir, "node_modules", "baz"))).toEqual(["index.js", "package.json"]);
expect(await file(join(ctx.package_dir, "node_modules", "baz", "package.json")).json()).toEqual({
name: "baz",
version: "0.0.3",
bin: {
"baz-run": "index.js",
},
});
await access(join(ctx.package_dir, "bun.lockb"));
// Perform `bun update` with updated registry & lockfile from before
await rm(join(ctx.package_dir, "node_modules"), { force: true, recursive: true });
urls.length = 0;
registry.latest = "0.0.5";
setContextHandler(ctx, dummyRegistryForContext(ctx, urls, registry));
const {
stdout: stdout2,
stderr: stderr2,
exited: exited2,
} = spawn({
cmd: [bunExe(), "update", "--linker=hoisted"],
cwd: ctx.package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const err2 = await new Response(stderr2).text();
expect(err2).not.toContain("error:");
expect(err2).toContain("Saved lockfile");
const out2 = await new Response(stdout2).text();
if (tilde) {
expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun update v1."),
"",
"^ baz 0.0.3 -> 0.0.5",
"",
"+ @barn/moo@0.1.0",
"",
"2 packages installed",
]);
} else {
expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun update v1."),
"",
expect.stringContaining("+ @barn/moo@0.1.0"),
expect.stringContaining("+ baz@0.0.3"),
"",
"2 packages installed",
]);
}
expect(await exited2).toBe(0);
expect(urls.sort()).toEqual([
`${ctx.registry_url}@barn%2fmoo`,
`${ctx.registry_url}@barn/moo-0.1.0.tgz`,
`${ctx.registry_url}baz`,
tilde ? `${ctx.registry_url}baz-0.0.5.tgz` : `${ctx.registry_url}baz-0.0.3.tgz`,
]);
expect(ctx.requested).toBe(8);
expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".bin", ".cache", "@barn", "baz"]);
expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins([
tilde ? "baz-exec" : "baz-run",
]);
expect(join(ctx.package_dir, "node_modules", ".bin", tilde ? "baz-exec" : "baz-run")).toBeValidBin(
join("..", "baz", "index.js"),
);
expect(await readdirSorted(join(ctx.package_dir, "node_modules", "@barn"))).toEqual(["moo"]);
expect(await readdirSorted(join(ctx.package_dir, "node_modules", "@barn", "moo"))).toEqual(["package.json"]);
expect(await readdirSorted(join(ctx.package_dir, "node_modules", "baz"))).toEqual(["index.js", "package.json"]);
expect(await file(join(ctx.package_dir, "node_modules", "baz", "package.json")).json()).toEqual({
name: "baz",
version: tilde ? "0.0.5" : "0.0.3",
bin: {
[tilde ? "baz-exec" : "baz-run"]: "index.js",
},
});
expect(await file(join(ctx.package_dir, "package.json")).json()).toEqual({
name: "foo",
dependencies: {
"@barn/moo": tilde ? "~0.1.0" : "^0.1.0",
baz: tilde ? "~0.0.5" : "^0.0.3",
},
});
await access(join(ctx.package_dir, "bun.lockb"));
});
});
}
// Assert we actually made a request to the registry for each update
expect(urls).toHaveLength(count);
});
it("lockfile should not be modified when there are no version changes, issue#5888", async () => {
await withContext(defaultOpts, async ctx => {
// Install packages
const urls: string[] = [];
const registry = {
"0.0.3": {
bin: {
"baz-run": "index.js",
},
},
latest: "0.0.3",
};
setContextHandler(ctx, dummyRegistryForContext(ctx, urls, registry));
await writeFile(
join(ctx.package_dir, "package.json"),
JSON.stringify({
name: "foo",
dependencies: {
baz: "0.0.3",
},
}),
);
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install", "--linker=hoisted"],
cwd: ctx.package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const err1 = await stderr.text();
expect(err1).not.toContain("error:");
expect(err1).toContain("Saved lockfile");
const out1 = await stdout.text();
expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun install v1."),
"",
"+ baz@0.0.3",
"",
"1 package installed",
]);
expect(await exited).toBe(0);
it("should support catalog versions in update", async () => {
const urls: string[] = [];
setHandler(dummyRegistry(urls));
// Test if the lockb has been modified by `bun update`.
const getLockbContent = async () => {
const { exited } = spawn({
cmd: [bunExe(), "update"],
cwd: ctx.package_dir, // package.json is not changed
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
expect(await exited).toBe(0);
return await readFile(join(ctx.package_dir, "bun.lockb"));
};
// Create a monorepo with catalog
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({
name: "root",
catalog: {
"no-deps": "^1.0.0",
},
workspaces: ["packages/*"],
}),
);
// no changes
expect(await file(join(ctx.package_dir, "package.json")).json()).toEqual({
name: "foo",
dependencies: {
baz: "0.0.3",
},
});
await mkdir(join(package_dir, "packages", "workspace-a"), { recursive: true });
await writeFile(
join(package_dir, "packages", "workspace-a", "package.json"),
JSON.stringify({
name: "workspace-a",
dependencies: {
"no-deps": "catalog:",
},
}),
);
let prev = await getLockbContent();
urls.length = 0;
const count = 5;
for (let i = 0; i < count; i++) {
const content = await getLockbContent();
expect(prev).toStrictEqual(content);
prev = content;
}
// Test that update works with catalog dependencies
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "update", "--dry-run"],
cwd: join(package_dir, "packages", "workspace-a"),
stdout: "pipe",
stderr: "pipe",
env,
// Assert we actually made a request to the registry for each update
expect(urls).toHaveLength(count);
});
});
const err = await new Response(stderr).text();
const out = await new Response(stdout).text();
it("should support catalog versions in update", async () => {
await withContext(defaultOpts, async ctx => {
const urls: string[] = [];
setContextHandler(ctx, dummyRegistryForContext(ctx, urls));
// Should not crash with catalog dependencies
expect(err).not.toContain("panic");
expect(err).not.toContain("segfault");
// Create a monorepo with catalog
await writeFile(
join(ctx.package_dir, "package.json"),
JSON.stringify({
name: "root",
catalog: {
"no-deps": "^1.0.0",
},
workspaces: ["packages/*"],
}),
);
// Verify catalog reference is preserved in package.json
const pkg = await file(join(package_dir, "packages", "workspace-a", "package.json")).json();
expect(pkg.dependencies["no-deps"]).toBe("catalog:");
});
await mkdir(join(ctx.package_dir, "packages", "workspace-a"), { recursive: true });
await writeFile(
join(ctx.package_dir, "packages", "workspace-a", "package.json"),
JSON.stringify({
name: "workspace-a",
dependencies: {
"no-deps": "catalog:",
},
}),
);
it("should support --recursive flag", async () => {
// First verify the flag appears in help
const {
stdout: helpOut,
stderr: helpErr,
exited: helpExited,
} = spawn({
cmd: [bunExe(), "update", "--help"],
cwd: package_dir,
stdout: "pipe",
stderr: "pipe",
env,
// Test that update works with catalog dependencies
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "update", "--dry-run"],
cwd: join(ctx.package_dir, "packages", "workspace-a"),
stdout: "pipe",
stderr: "pipe",
env,
});
const err = await new Response(stderr).text();
const out = await new Response(stdout).text();
// Should not crash with catalog dependencies
expect(err).not.toContain("panic");
expect(err).not.toContain("segfault");
// Verify catalog reference is preserved in package.json
const pkg = await file(join(ctx.package_dir, "packages", "workspace-a", "package.json")).json();
expect(pkg.dependencies["no-deps"]).toBe("catalog:");
});
});
const help = (await new Response(helpOut).text()) + (await new Response(helpErr).text());
expect(await helpExited).toBe(0);
expect(help).toContain("--recursive");
expect(help).toContain("-r");
it("should support --recursive flag", async () => {
await withContext(defaultOpts, async ctx => {
// First verify the flag appears in help
const {
stdout: helpOut,
stderr: helpErr,
exited: helpExited,
} = spawn({
cmd: [bunExe(), "update", "--help"],
cwd: ctx.package_dir,
stdout: "pipe",
stderr: "pipe",
env,
});
// Now test that --recursive actually works
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({
name: "root",
workspaces: ["packages/*"],
dependencies: {
"no-deps": "^1.0.0",
},
}),
);
const help = (await new Response(helpOut).text()) + (await new Response(helpErr).text());
expect(help).toContain("--recursive");
expect(help).toContain("-r");
expect(await helpExited).toBe(0);
await mkdir(join(package_dir, "packages", "pkg1"), { recursive: true });
await writeFile(
join(package_dir, "packages", "pkg1", "package.json"),
JSON.stringify({
name: "pkg1",
dependencies: {
"no-deps": "^1.0.0",
},
}),
);
// Now test that --recursive actually works
await writeFile(
join(ctx.package_dir, "package.json"),
JSON.stringify({
name: "root",
workspaces: ["packages/*"],
dependencies: {
"no-deps": "^1.0.0",
},
}),
);
// Test recursive update (might fail without lockfile, but shouldn't crash)
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "update", "--recursive", "--dry-run"],
cwd: package_dir,
stdout: "pipe",
stderr: "pipe",
env,
await mkdir(join(ctx.package_dir, "packages", "pkg1"), { recursive: true });
await writeFile(
join(ctx.package_dir, "packages", "pkg1", "package.json"),
JSON.stringify({
name: "pkg1",
dependencies: {
"no-deps": "^1.0.0",
},
}),
);
// Test recursive update (might fail without lockfile, but shouldn't crash)
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "update", "--recursive", "--dry-run"],
cwd: ctx.package_dir,
stdout: "pipe",
stderr: "pipe",
env,
});
const out = await new Response(stdout).text();
const err = await new Response(stderr).text();
// Should not crash
expect(err).not.toContain("panic");
expect(err).not.toContain("segfault");
// Should recognize the flag (either process workspaces or show error about missing lockfile)
expect(out + err).toMatch(/bun update|missing lockfile|nothing to update/);
});
});
const out = await new Response(stdout).text();
const err = await new Response(stderr).text();
// Should not crash
expect(err).not.toContain("panic");
expect(err).not.toContain("segfault");
// Should recognize the flag (either process workspaces or show error about missing lockfile)
expect(out + err).toMatch(/bun update|missing lockfile|nothing to update/);
});

View File

@@ -5,7 +5,15 @@ import { bunEnv, bunExe, isWindows, readdirSorted, tmpdirSync } from "harness";
import { copyFileSync, readdirSync } from "node:fs";
import { tmpdir } from "os";
import { join, resolve } from "path";
import { dummyAfterAll, dummyBeforeAll, dummyBeforeEach, dummyRegistry, getPort, setHandler } from "./dummy.registry";
import {
createTestContext,
destroyTestContext,
dummyAfterAll,
dummyBeforeAll,
dummyRegistryForContext,
setContextHandler,
type TestContext,
} from "./dummy.registry";
let x_dir: string;
let current_tmpdir: string;
@@ -523,272 +531,267 @@ describe("--package flag", () => {
expect(exited).toBe(1);
});
describe("with mock registry", () => {
let port: number;
describe.concurrent("with mock registry", () => {
beforeAll(() => {
dummyBeforeAll();
port = getPort()!;
});
afterAll(() => {
dummyAfterAll();
});
beforeEach(async () => {
await dummyBeforeEach();
});
async function withContext(
opts: { linker?: "hoisted" | "isolated" } | undefined,
fn: (ctx: TestContext) => Promise<void>,
): Promise<void> {
const ctx = await createTestContext(opts ? { linker: opts.linker! } : undefined);
try {
await fn(ctx);
} finally {
destroyTestContext(ctx);
}
}
const runWithRegistry = async (
...args: string[]
): Promise<[err: string, out: string, exited: number, urls: string[]]> => {
const urls: string[] = [];
const subprocess = spawn({
cmd: [bunExe(), "x", ...args],
cwd: x_dir,
stdout: "pipe",
stdin: "inherit",
stderr: "pipe",
env: {
...env,
npm_config_registry: `http://localhost:${port}/`,
},
});
const [err, out, exited] = await Promise.all([
subprocess.stderr.text(),
subprocess.stdout.text(),
subprocess.exited,
]);
return [err, out, exited, urls];
};
const defaultOpts = { linker: "hoisted" as const };
it("should install specified package when binary differs from package name", async () => {
const urls: string[] = [];
await withContext(defaultOpts, async ctx => {
const urls: string[] = [];
// Set up dummy registry with a package that has a different binary name
setHandler(
dummyRegistry(urls, {
"1.0.0": {
bin: {
"different-bin": "index.js",
// Set up dummy registry with a package that has a different binary name
setContextHandler(
ctx,
dummyRegistryForContext(ctx, urls, {
"1.0.0": {
bin: {
"different-bin": "index.js",
},
as: "1.0.0",
},
as: "1.0.0",
}),
);
// Tarball already exists in test directory
// Without --package, bunx different-bin would fail
// With --package, we correctly install my-special-pkg
const subprocess = spawn({
cmd: [bunExe(), "x", "--package", "my-special-pkg", "different-bin", "--help"],
cwd: ctx.package_dir,
stdout: "pipe",
stdin: "inherit",
stderr: "pipe",
env: {
...env,
npm_config_registry: ctx.registry_url,
},
}),
);
});
// Tarball already exists in test directory
const [err, out, exited] = await Promise.all([
subprocess.stderr.text(),
subprocess.stdout.text(),
subprocess.exited,
]);
// Without --package, bunx different-bin would fail
// With --package, we correctly install my-special-pkg
const subprocess = spawn({
cmd: [bunExe(), "x", "--package", "my-special-pkg", "different-bin", "--help"],
cwd: x_dir,
stdout: "pipe",
stdin: "inherit",
stderr: "pipe",
env: {
...env,
npm_config_registry: `http://localhost:${port}/`,
},
expect(urls.some(url => url.includes("/my-special-pkg"))).toBe(true);
// The package should install successfully
expect(err).toContain("Saved lockfile");
});
const [err, out, exited] = await Promise.all([
subprocess.stderr.text(),
subprocess.stdout.text(),
subprocess.exited,
]);
expect(urls.some(url => url.includes("/my-special-pkg"))).toBe(true);
// The package should install successfully
expect(err).toContain("Saved lockfile");
});
it("should support -p shorthand with mock registry", async () => {
const urls: string[] = [];
await withContext(defaultOpts, async ctx => {
const urls: string[] = [];
setHandler(
dummyRegistry(urls, {
"2.0.0": {
bin: {
"tool": "cli.js",
setContextHandler(
ctx,
dummyRegistryForContext(ctx, urls, {
"2.0.0": {
bin: {
tool: "cli.js",
},
as: "2.0.0",
},
as: "2.0.0",
}),
);
// Tarball already exists in test directory
const subprocess = spawn({
cmd: [bunExe(), "x", "-p", "actual-package", "tool", "--version"],
cwd: ctx.package_dir,
stdout: "pipe",
stdin: "inherit",
stderr: "pipe",
env: {
...env,
npm_config_registry: ctx.registry_url,
},
}),
);
});
// Tarball already exists in test directory
const [err, out, exited] = await Promise.all([
subprocess.stderr.text(),
subprocess.stdout.text(),
subprocess.exited,
]);
const subprocess = spawn({
cmd: [bunExe(), "x", "-p", "actual-package", "tool", "--version"],
cwd: x_dir,
stdout: "pipe",
stdin: "inherit",
stderr: "pipe",
env: {
...env,
npm_config_registry: `http://localhost:${port}/`,
},
expect(urls.some(url => url.includes("/actual-package"))).toBe(true);
});
const [err, out, exited] = await Promise.all([
subprocess.stderr.text(),
subprocess.stdout.text(),
subprocess.exited,
]);
expect(urls.some(url => url.includes("/actual-package"))).toBe(true);
});
it("should support --package=<pkg> syntax with mock registry", async () => {
const urls: string[] = [];
await withContext(defaultOpts, async ctx => {
const urls: string[] = [];
setHandler(
dummyRegistry(urls, {
"3.0.0": {
bin: {
"runner": "run.js",
setContextHandler(
ctx,
dummyRegistryForContext(ctx, urls, {
"3.0.0": {
bin: {
runner: "run.js",
},
as: "3.0.0",
},
as: "3.0.0",
}),
);
// Tarball already exists in test directory
const subprocess = spawn({
cmd: [bunExe(), "x", "--package=runner-pkg", "runner", "--help"],
cwd: ctx.package_dir,
stdout: "pipe",
stdin: "inherit",
stderr: "pipe",
env: {
...env,
npm_config_registry: ctx.registry_url,
},
}),
);
});
// Tarball already exists in test directory
const [err, out, exited] = await Promise.all([
subprocess.stderr.text(),
subprocess.stdout.text(),
subprocess.exited,
]);
const subprocess = spawn({
cmd: [bunExe(), "x", "--package=runner-pkg", "runner", "--help"],
cwd: x_dir,
stdout: "pipe",
stdin: "inherit",
stderr: "pipe",
env: {
...env,
npm_config_registry: `http://localhost:${port}/`,
},
expect(urls.some(url => url.includes("/runner-pkg"))).toBe(true);
});
const [err, out, exited] = await Promise.all([
subprocess.stderr.text(),
subprocess.stdout.text(),
subprocess.exited,
]);
expect(urls.some(url => url.includes("/runner-pkg"))).toBe(true);
});
it("should fail to run alternate binary without --package flag", async () => {
// Attempt to run multi-tool-alt without --package flag
// This should fail because bunx would try to install a package named "multi-tool-alt"
const subprocess = spawn({
cmd: [bunExe(), "x", "multi-tool-alt"],
cwd: x_dir,
stdout: "pipe",
stdin: "inherit",
stderr: "pipe",
env: {
...env,
npm_config_registry: `http://localhost:${port}/`,
},
await withContext(defaultOpts, async ctx => {
// Attempt to run multi-tool-alt without --package flag
// This should fail because bunx would try to install a package named "multi-tool-alt"
const subprocess = spawn({
cmd: [bunExe(), "x", "multi-tool-alt"],
cwd: ctx.package_dir,
stdout: "pipe",
stdin: "inherit",
stderr: "pipe",
env: {
...env,
npm_config_registry: ctx.registry_url,
},
});
const [err, _out, exited] = await Promise.all([
subprocess.stderr.text(),
subprocess.stdout.text(),
subprocess.exited,
]);
// Should fail because there's no package named "multi-tool-alt"
expect(err).toContain("error:");
expect(exited).not.toBe(0);
});
const [err, _out, exited] = await Promise.all([
subprocess.stderr.text(),
subprocess.stdout.text(),
subprocess.exited,
]);
// Should fail because there's no package named "multi-tool-alt"
expect(err).toContain("error:");
expect(exited).not.toBe(0);
});
it("should execute the correct binary when package has multiple binaries", async () => {
const urls: string[] = [];
await withContext(defaultOpts, async ctx => {
const urls: string[] = [];
// Set up a package with two different binaries
setHandler(
dummyRegistry(urls, {
"1.0.0": {
// Set up a package with two different binaries
setContextHandler(
ctx,
dummyRegistryForContext(ctx, urls, {
"1.0.0": {
bin: {
"multi-tool": "bin/multi-tool.js",
"multi-tool-alt": "bin/multi-tool-alt.js",
},
as: "1.0.0",
},
}),
);
// Create the tarball with both binaries that output different messages
// First, let's create the package structure
const tempDir = tmpdirSync();
const packageDir = join(tempDir, "package");
await Bun.$`mkdir -p ${packageDir}/bin`;
await writeFile(
join(packageDir, "package.json"),
JSON.stringify({
name: "multi-tool-pkg",
version: "1.0.0",
bin: {
"multi-tool": "bin/multi-tool.js",
"multi-tool-alt": "bin/multi-tool-alt.js",
},
as: "1.0.0",
},
}),
);
}),
);
// Create the tarball with both binaries that output different messages
// First, let's create the package structure
const tempDir = tmpdirSync();
const packageDir = join(tempDir, "package");
await Bun.$`mkdir -p ${packageDir}/bin`;
await writeFile(
join(packageDir, "package.json"),
JSON.stringify({
name: "multi-tool-pkg",
version: "1.0.0",
bin: {
"multi-tool": "bin/multi-tool.js",
"multi-tool-alt": "bin/multi-tool-alt.js",
},
}),
);
await writeFile(
join(packageDir, "bin", "multi-tool.js"),
`#!/usr/bin/env node
await writeFile(
join(packageDir, "bin", "multi-tool.js"),
`#!/usr/bin/env node
console.log("EXECUTED: multi-tool (main binary)");
`,
);
);
await writeFile(
join(packageDir, "bin", "multi-tool-alt.js"),
`#!/usr/bin/env node
await writeFile(
join(packageDir, "bin", "multi-tool-alt.js"),
`#!/usr/bin/env node
console.log("EXECUTED: multi-tool-alt (alternate binary)");
`,
);
);
// Make the binaries executable
await Bun.$`chmod +x ${packageDir}/bin/multi-tool.js ${packageDir}/bin/multi-tool-alt.js`;
// Make the binaries executable
await Bun.$`chmod +x ${packageDir}/bin/multi-tool.js ${packageDir}/bin/multi-tool-alt.js`;
// Create the tarball with package/ prefix
await Bun.$`cd ${tempDir} && tar -czf ${join(import.meta.dir, "multi-tool-pkg-1.0.0.tgz")} package`;
// Create the tarball with package/ prefix
await Bun.$`cd ${tempDir} && tar -czf ${join(import.meta.dir, "multi-tool-pkg-1.0.0.tgz")} package`;
// Test 1: Without --package, bunx multi-tool-alt should fail or install wrong package
// Test 2: With --package, we can run the alternate binary
const subprocess = spawn({
cmd: [bunExe(), "x", "--package", "multi-tool-pkg", "multi-tool-alt"],
cwd: x_dir,
stdout: "pipe",
stdin: "inherit",
stderr: "pipe",
env: {
...env,
npm_config_registry: `http://localhost:${port}/`,
},
// Test 1: Without --package, bunx multi-tool-alt should fail or install wrong package
// Test 2: With --package, we can run the alternate binary
const subprocess = spawn({
cmd: [bunExe(), "x", "--package", "multi-tool-pkg", "multi-tool-alt"],
cwd: ctx.package_dir,
stdout: "pipe",
stdin: "inherit",
stderr: "pipe",
env: {
...env,
npm_config_registry: ctx.registry_url,
},
});
const [_err, out, exited] = await Promise.all([
subprocess.stderr.text(),
subprocess.stdout.text(),
subprocess.exited,
]);
// Verify the correct package was requested
expect(urls.some(url => url.includes("/multi-tool-pkg"))).toBe(true);
// Verify the correct binary was executed
expect(out).toContain("EXECUTED: multi-tool-alt (alternate binary)");
expect(out).not.toContain("EXECUTED: multi-tool (main binary)");
expect(exited).toBe(0);
});
const [_err, out, exited] = await Promise.all([
subprocess.stderr.text(),
subprocess.stdout.text(),
subprocess.exited,
]);
// Verify the correct package was requested
expect(urls.some(url => url.includes("/multi-tool-pkg"))).toBe(true);
// Verify the correct binary was executed
expect(out).toContain("EXECUTED: multi-tool-alt (alternate binary)");
expect(out).not.toContain("EXECUTED: multi-tool (main binary)");
expect(exited).toBe(0);
});
});
});

View File

@@ -1,83 +1,102 @@
import { spawn } from "bun";
import { afterAll, afterEach, beforeAll, beforeEach, expect, it } from "bun:test";
import { afterAll, beforeAll, describe, expect, it, setDefaultTimeout } from "bun:test";
import { access, writeFile } from "fs/promises";
import { bunExe, bunEnv as env } from "harness";
import { join } from "path";
import {
createTestContext,
destroyTestContext,
dummyAfterAll,
dummyAfterEach,
dummyBeforeAll,
dummyBeforeEach,
dummyRegistry,
package_dir,
requested,
root_url,
setHandler,
} from "./dummy.registry.js";
dummyRegistryForContext,
setContextHandler,
type TestContext,
} from "./dummy.registry";
beforeAll(dummyBeforeAll);
afterAll(dummyAfterAll);
beforeEach(async () => {
await dummyBeforeEach();
beforeAll(() => {
setDefaultTimeout(1000 * 60 * 5);
dummyBeforeAll();
});
afterEach(dummyAfterEach);
afterAll(dummyAfterAll);
it.each(["bun.lockb", "bun.lock"])("should not download tarballs with --lockfile-only using %s", async lockfile => {
const isLockb = lockfile === "bun.lockb";
// Helper function that sets up test context and ensures cleanup
async function withContext(
opts: { linker?: "hoisted" | "isolated" } | undefined,
fn: (ctx: TestContext) => Promise<void>,
): Promise<void> {
const ctx = await createTestContext(opts ? { linker: opts.linker! } : undefined);
try {
await fn(ctx);
} finally {
destroyTestContext(ctx);
}
}
const urls: string[] = [];
const registry = { "0.0.1": { as: "0.0.1" }, latest: "0.0.1" };
// Default context options for most tests
const defaultOpts = { linker: "hoisted" as const };
setHandler(dummyRegistry(urls, registry));
describe.concurrent("lockfile-only", () => {
for (const lockfile of ["bun.lockb", "bun.lock"]) {
it(`should not download tarballs with --lockfile-only using ${lockfile}`, async () => {
await withContext(defaultOpts, async ctx => {
const isLockb = lockfile === "bun.lockb";
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({
name: "foo",
dependencies: {
baz: "0.0.1",
},
}),
);
const urls: string[] = [];
const registry = { "0.0.1": { as: "0.0.1" }, latest: "0.0.1" };
const cmd = [bunExe(), "install", "--lockfile-only"];
setContextHandler(ctx, dummyRegistryForContext(ctx, urls, registry));
if (!isLockb) {
// the default beforeEach disables --save-text-lockfile in the dummy registry, so we should restore
// default behaviour
await writeFile(
join(package_dir, "bunfig.toml"),
`
await writeFile(
join(ctx.package_dir, "package.json"),
JSON.stringify({
name: "foo",
dependencies: {
baz: "0.0.1",
},
}),
);
const cmd = [bunExe(), "install", "--lockfile-only"];
if (!isLockb) {
// the default beforeEach disables --save-text-lockfile in the dummy registry, so we should restore
// default behaviour
await writeFile(
join(ctx.package_dir, "bunfig.toml"),
`
[install]
cache = false
registry = "${root_url}/"
registry = "${ctx.registry_url}"
`,
);
);
}
const { stdout, stderr, exited } = spawn({
cmd,
cwd: ctx.package_dir,
stdout: "pipe",
stderr: "pipe",
env,
});
const err = await stderr.text();
const out = await stdout.text();
expect(err).not.toContain("error:");
expect(err).toContain("Saved lockfile");
expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun install v1."),
"",
expect.stringContaining(`Saved ${lockfile}`),
]);
expect(urls.sort()).toEqual([`${ctx.registry_url}baz`]);
expect(ctx.requested).toBe(1);
await access(join(ctx.package_dir, lockfile));
expect(await exited).toBe(0);
});
});
}
const { stdout, stderr, exited } = spawn({
cmd,
cwd: package_dir,
stdout: "pipe",
stderr: "pipe",
env,
});
expect(await exited).toBe(0);
const err = await stderr.text();
expect(err).not.toContain("error:");
expect(err).toContain("Saved lockfile");
const out = await stdout.text();
expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun install v1."),
"",
expect.stringContaining(`Saved ${lockfile}`),
]);
expect(urls.sort()).toEqual([`${root_url}/baz`]);
expect(requested).toBe(1);
await access(join(package_dir, lockfile));
});

View File

@@ -1,81 +1,97 @@
import { file, spawn } from "bun";
import { afterAll, afterEach, beforeAll, beforeEach, expect, it } from "bun:test";
import { afterAll, beforeAll, describe, expect, it, setDefaultTimeout } from "bun:test";
import { access, writeFile } from "fs/promises";
import { bunExe, bunEnv as env, readdirSorted } from "harness";
import { join } from "path";
import {
createTestContext,
destroyTestContext,
dummyAfterAll,
dummyAfterEach,
dummyBeforeAll,
dummyBeforeEach,
dummyRegistry,
package_dir,
requested,
root_url,
setHandler,
} from "./../../cli/install/dummy.registry.js";
dummyRegistryForContext,
setContextHandler,
type TestContext,
} from "./../../cli/install/dummy.registry";
beforeAll(dummyBeforeAll);
beforeAll(() => {
setDefaultTimeout(1000 * 60 * 5);
dummyBeforeAll();
});
afterAll(dummyAfterAll);
beforeEach(async () => {
await dummyBeforeEach();
});
afterEach(dummyAfterEach);
it("should install vendored node_modules with hardlink", async () => {
const urls: string[] = [];
setHandler(
dummyRegistry(urls, {
"0.0.1": {},
latest: "0.0.1",
}),
);
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({
name: "foo",
version: "0.0.1",
dependencies: {
"vendor-baz": "0.0.1",
},
}),
);
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install", "--backend", "hardlink", "--linker=hoisted"],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
async function withContext(
opts: { linker?: "hoisted" | "isolated" } | undefined,
fn: (ctx: TestContext) => Promise<void>,
): Promise<void> {
const ctx = await createTestContext(opts ? { linker: opts.linker! } : undefined);
try {
await fn(ctx);
} finally {
destroyTestContext(ctx);
}
}
const defaultOpts = { linker: "hoisted" as const };
describe.concurrent("issue-08093", () => {
it("should install vendored node_modules with hardlink", async () => {
await withContext(defaultOpts, async ctx => {
const urls: string[] = [];
setContextHandler(
ctx,
dummyRegistryForContext(ctx, urls, {
"0.0.1": {},
latest: "0.0.1",
}),
);
await writeFile(
join(ctx.package_dir, "package.json"),
JSON.stringify({
name: "foo",
version: "0.0.1",
dependencies: {
"vendor-baz": "0.0.1",
},
}),
);
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install", "--backend", "hardlink", "--linker=hoisted"],
cwd: ctx.package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
expect(stderr).toBeDefined();
const err = await stderr.text();
expect(err).toContain("Saved lockfile");
expect(stdout).toBeDefined();
const out = await stdout.text();
expect(out).toContain("1 package installed");
expect(await exited).toBe(0);
expect(urls.sort()).toEqual([`${ctx.registry_url}vendor-baz`, `${ctx.registry_url}vendor-baz-0.0.1.tgz`]);
expect(ctx.requested).toBe(2);
expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "vendor-baz"]);
expect(await readdirSorted(join(ctx.package_dir, "node_modules", "vendor-baz"))).toEqual([
"cjs",
"index.js",
"package.json",
]);
expect(await readdirSorted(join(ctx.package_dir, "node_modules", "vendor-baz", "cjs", "node_modules"))).toEqual([
"foo-dep",
]);
expect(
await readdirSorted(join(ctx.package_dir, "node_modules", "vendor-baz", "cjs", "node_modules", "foo-dep")),
).toEqual(["index.js"]);
expect(await file(join(ctx.package_dir, "node_modules", "vendor-baz", "package.json")).json()).toEqual({
name: "vendor-baz",
version: "0.0.1",
});
await access(join(ctx.package_dir, "bun.lockb"));
});
});
expect(stderr).toBeDefined();
const err = await stderr.text();
expect(err).toContain("Saved lockfile");
expect(stdout).toBeDefined();
const out = await stdout.text();
expect(out).toContain("1 package installed");
expect(await exited).toBe(0);
expect(urls.sort()).toEqual([`${root_url}/vendor-baz`, `${root_url}/vendor-baz-0.0.1.tgz`]);
expect(requested).toBe(2);
expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "vendor-baz"]);
expect(await readdirSorted(join(package_dir, "node_modules", "vendor-baz"))).toEqual([
"cjs",
"index.js",
"package.json",
]);
expect(await readdirSorted(join(package_dir, "node_modules", "vendor-baz", "cjs", "node_modules"))).toEqual([
"foo-dep",
]);
expect(
await readdirSorted(join(package_dir, "node_modules", "vendor-baz", "cjs", "node_modules", "foo-dep")),
).toEqual(["index.js"]);
expect(await file(join(package_dir, "node_modules", "vendor-baz", "package.json")).json()).toEqual({
name: "vendor-baz",
version: "0.0.1",
});
await access(join(package_dir, "bun.lockb"));
});