mirror of
https://github.com/oven-sh/bun
synced 2026-02-16 22:01:47 +00:00
Compare commits
2 Commits
claude/fix
...
claude/con
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0a30961fe | ||
|
|
b7b1507d52 |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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
@@ -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",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
|
||||
Binary file not shown.
@@ -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"));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user