mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
### What does this PR do? - Implements .onEnd Fixes #22061 Once #22144 is merged, this also fixes: Fixes #9862 Fixes #20806 ### How did you verify your code works? Tests --- TODO in a followup (#22144) > ~~Make all entrypoints be called in onResolve~~ > ~~Fixes # 9862~~ > ~~Fixes # 20806~~ --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1122 lines
33 KiB
TypeScript
1122 lines
33 KiB
TypeScript
import assert from "assert";
|
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
import { readFileSync, writeFileSync } from "fs";
|
|
import { bunEnv, bunExe, tempDirWithFiles, tempDirWithFilesAnon } from "harness";
|
|
import path, { join } from "path";
|
|
import { buildNoThrow } from "./buildNoThrow";
|
|
|
|
describe("Bun.build", () => {
|
|
test("css works", async () => {
|
|
const dir = tempDirWithFiles("bun-build-api-css", {
|
|
"a.css": `
|
|
@import "./b.css";
|
|
|
|
.hi {
|
|
color: red;
|
|
}
|
|
`,
|
|
"b.css": `
|
|
.hello {
|
|
color: blue;
|
|
}
|
|
`,
|
|
});
|
|
|
|
const build = await Bun.build({
|
|
entrypoints: [join(dir, "a.css")],
|
|
minify: true,
|
|
});
|
|
|
|
expect(build.outputs).toHaveLength(1);
|
|
expect(build.outputs[0].kind).toBe("asset");
|
|
expect(await build.outputs[0].text()).toEqualIgnoringWhitespace(".hello{color:#00f}.hi{color:red}\n");
|
|
});
|
|
|
|
test("bytecode works", async () => {
|
|
const dir = tempDirWithFiles("bun-build-api-bytecode", {
|
|
"package.json": `{}`,
|
|
"index.ts": `
|
|
export function hello() {
|
|
return "world";
|
|
}
|
|
|
|
console.log(hello());
|
|
`,
|
|
out: {
|
|
"hmm.js": "hmm",
|
|
},
|
|
});
|
|
|
|
const build = await Bun.build({
|
|
entrypoints: [join(dir, "index.ts")],
|
|
outdir: join(dir, "out"),
|
|
target: "bun",
|
|
bytecode: true,
|
|
});
|
|
|
|
expect(build.outputs).toHaveLength(2);
|
|
expect(build.outputs[0].kind).toBe("entry-point");
|
|
expect(build.outputs[1].kind).toBe("bytecode");
|
|
expect([build.outputs[0].path]).toRun("world\n");
|
|
});
|
|
|
|
test("passing undefined doesnt segfault", () => {
|
|
try {
|
|
// @ts-ignore
|
|
Bun.build();
|
|
} catch (error) {
|
|
return;
|
|
}
|
|
throw new Error("should have thrown");
|
|
});
|
|
|
|
// https://github.com/oven-sh/bun/issues/12818
|
|
test("sourcemap + build error crash case", async () => {
|
|
const dir = tempDirWithFiles("build", {
|
|
"/src/file1.ts": `
|
|
import { A } from './dir';
|
|
console.log(A);
|
|
`,
|
|
"/src/dir/index.ts": `
|
|
import { B } from "./file3";
|
|
export const A = [B]
|
|
`,
|
|
"/src/dir/file3.ts": `
|
|
import { C } from "../file1"; // error
|
|
export const B = C;
|
|
`,
|
|
"/src/package.json": `
|
|
{ "type": "module" }
|
|
`,
|
|
"/src/tsconfig.json": `
|
|
{
|
|
"extends": "../tsconfig.json",
|
|
"compilerOptions": {
|
|
"target": "ESNext",
|
|
"module": "ESNext",
|
|
"types": []
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
const y = await buildNoThrow({
|
|
entrypoints: [join(dir, "src/file1.ts")],
|
|
outdir: join(dir, "out"),
|
|
sourcemap: "external",
|
|
external: ["@minecraft"],
|
|
});
|
|
});
|
|
|
|
test("invalid options throws", async () => {
|
|
expect(() => Bun.build({} as any)).toThrow();
|
|
expect(() =>
|
|
Bun.build({
|
|
entrypoints: [],
|
|
} as any),
|
|
).toThrow();
|
|
expect(() =>
|
|
Bun.build({
|
|
entrypoints: ["hello"],
|
|
format: "invalid",
|
|
} as any),
|
|
).toThrow();
|
|
expect(() =>
|
|
Bun.build({
|
|
entrypoints: ["hello"],
|
|
target: "invalid",
|
|
} as any),
|
|
).toThrow();
|
|
expect(() =>
|
|
Bun.build({
|
|
entrypoints: ["hello"],
|
|
sourcemap: "invalid",
|
|
} as any),
|
|
).toThrow();
|
|
});
|
|
|
|
test("returns errors properly", async () => {
|
|
Bun.gc(true);
|
|
const build = await buildNoThrow({
|
|
entrypoints: [join(import.meta.dir, "does-not-exist.ts")],
|
|
});
|
|
expect(build.outputs).toHaveLength(0);
|
|
expect(build.logs).toHaveLength(1);
|
|
expect(build.logs[0]).toBeInstanceOf(BuildMessage);
|
|
expect(build.logs[0].message).toMatch(/ModuleNotFound/);
|
|
expect(build.logs[0].name).toBe("BuildMessage");
|
|
expect(build.logs[0].position).toEqual(null);
|
|
expect(build.logs[0].level).toEqual("error");
|
|
Bun.gc(true);
|
|
});
|
|
|
|
test("errors are thrown", async () => {
|
|
Bun.gc(true);
|
|
try {
|
|
await Bun.build({
|
|
entrypoints: [join(import.meta.dir, "does-not-exist.ts")],
|
|
});
|
|
expect.unreachable();
|
|
} catch (e) {
|
|
assert(e instanceof AggregateError);
|
|
expect(e.errors).toHaveLength(1);
|
|
expect(e.errors[0]).toBeInstanceOf(BuildMessage);
|
|
expect(e.errors[0].message).toMatch(/ModuleNotFound/);
|
|
expect(e.errors[0].name).toBe("BuildMessage");
|
|
expect(e.errors[0].position).toEqual(null);
|
|
expect(e.errors[0].level).toEqual("error");
|
|
Bun.gc(true);
|
|
}
|
|
});
|
|
|
|
test("returns output files", async () => {
|
|
Bun.gc(true);
|
|
const build = await Bun.build({
|
|
entrypoints: [join(import.meta.dir, "./fixtures/trivial/index.js")],
|
|
});
|
|
expect(build.outputs).toHaveLength(1);
|
|
expect(build.logs).toHaveLength(0);
|
|
Bun.gc(true);
|
|
});
|
|
|
|
test("Bun.write(BuildArtifact)", async () => {
|
|
Bun.gc(true);
|
|
const tmpdir = tempDirWithFiles("bun-build-api-write", {
|
|
"package.json": `{}`,
|
|
});
|
|
const x = await Bun.build({
|
|
entrypoints: [join(import.meta.dir, "./fixtures/trivial/index.js")],
|
|
});
|
|
await Bun.write(path.join(tmpdir, "index.js"), x.outputs[0]);
|
|
expect(readFileSync(path.join(tmpdir, "index.js"), "utf-8")).toMatchSnapshot();
|
|
Bun.gc(true);
|
|
});
|
|
|
|
test("rebuilding busts the directory entries cache", () => {
|
|
Bun.gc(true);
|
|
const tmpdir = tempDirWithFiles("rebuild-bust-dirent-cache", {
|
|
"package.json": `{}`,
|
|
});
|
|
|
|
const { exitCode, stderr } = Bun.spawnSync({
|
|
cmd: [bunExe(), join(import.meta.dir, "fixtures", "bundler-reloader-script.ts")],
|
|
env: { ...bunEnv, BUNDLER_RELOADER_SCRIPT_TMP_DIR: tmpdir },
|
|
stderr: "pipe",
|
|
stdout: "inherit",
|
|
});
|
|
if (stderr.byteLength > 0) {
|
|
throw new Error(stderr.toString());
|
|
}
|
|
expect(exitCode).toBe(0);
|
|
Bun.gc(true);
|
|
});
|
|
|
|
test("outdir + reading out blobs works", async () => {
|
|
Bun.gc(true);
|
|
const fixture = tempDirWithFiles("build-outdir", {
|
|
"package.json": `{}`,
|
|
});
|
|
const x = await Bun.build({
|
|
entrypoints: [join(import.meta.dir, "./fixtures/trivial/index.js")],
|
|
outdir: fixture,
|
|
});
|
|
expect(await x.outputs.values().next().value?.text()).toMatchSnapshot();
|
|
Bun.gc(true);
|
|
});
|
|
|
|
test("BuildArtifact properties", async () => {
|
|
Bun.gc(true);
|
|
const outdir = tempDirWithFiles("build-artifact-properties", {
|
|
"package.json": `{}`,
|
|
});
|
|
const x = await Bun.build({
|
|
entrypoints: [join(import.meta.dir, "./fixtures/trivial/index.js")],
|
|
outdir,
|
|
});
|
|
console.log(await x.outputs[0].text());
|
|
const [blob] = x.outputs;
|
|
expect(blob).toBeTruthy();
|
|
expect(blob.type).toBe("text/javascript;charset=utf-8");
|
|
expect(blob.size).toBeGreaterThan(1);
|
|
expect(path.relative(outdir, blob.path)).toBe("index.js");
|
|
expect(blob.hash).toBeTruthy();
|
|
expect(blob.hash).toMatchSnapshot("hash");
|
|
expect(blob.kind).toBe("entry-point");
|
|
expect(blob.loader).toBe("jsx");
|
|
expect(blob.sourcemap).toBe(null);
|
|
Bun.gc(true);
|
|
});
|
|
|
|
test("BuildArtifact properties + entry.naming", async () => {
|
|
Bun.gc(true);
|
|
const outdir = tempDirWithFiles("build-artifact-properties-entry-naming", {
|
|
"package.json": `{}`,
|
|
});
|
|
const x = await Bun.build({
|
|
entrypoints: [join(import.meta.dir, "./fixtures/trivial/index.js")],
|
|
naming: {
|
|
entry: "hello",
|
|
},
|
|
outdir,
|
|
});
|
|
const [blob] = x.outputs;
|
|
expect(blob).toBeTruthy();
|
|
expect(blob.type).toBe("text/javascript;charset=utf-8");
|
|
expect(blob.size).toBeGreaterThan(1);
|
|
expect(path.relative(outdir, blob.path)).toBe("hello");
|
|
expect(blob.hash).toBeTruthy();
|
|
expect(blob.hash).toMatchSnapshot("hash");
|
|
expect(blob.kind).toBe("entry-point");
|
|
expect(blob.loader).toBe("jsx");
|
|
expect(blob.sourcemap).toBe(null);
|
|
Bun.gc(true);
|
|
});
|
|
|
|
test("BuildArtifact properties sourcemap", async () => {
|
|
Bun.gc(true);
|
|
const outdir = tempDirWithFiles("build-artifact-properties-sourcemap", {
|
|
"package.json": `{}`,
|
|
});
|
|
const x = await Bun.build({
|
|
entrypoints: [join(import.meta.dir, "./fixtures/trivial/index.js")],
|
|
sourcemap: "external",
|
|
outdir,
|
|
});
|
|
const [blob, map] = x.outputs;
|
|
expect(blob.type).toBe("text/javascript;charset=utf-8");
|
|
expect(blob.size).toBeGreaterThan(1);
|
|
expect(path.relative(outdir, blob.path)).toBe("index.js");
|
|
expect(blob.hash).toBeTruthy();
|
|
expect(blob.hash).toMatchSnapshot("hash index.js");
|
|
expect(blob.kind).toBe("entry-point");
|
|
expect(blob.loader).toBe("jsx");
|
|
expect(blob.sourcemap).toBe(map);
|
|
|
|
expect(map.type).toBe("application/json;charset=utf-8");
|
|
expect(map.size).toBeGreaterThan(1);
|
|
expect(path.relative(outdir, map.path)).toBe("index.js.map");
|
|
expect(map.hash).toBeTruthy();
|
|
expect(map.hash).toMatchSnapshot("hash index.js.map");
|
|
expect(map.kind).toBe("sourcemap");
|
|
expect(map.loader).toBe("file");
|
|
expect(map.sourcemap).toBe(null);
|
|
Bun.gc(true);
|
|
});
|
|
|
|
// test("BuildArtifact properties splitting", async () => {
|
|
// Bun.gc(true);
|
|
// const x = await Bun.build({
|
|
// entrypoints: [join(import.meta.dir, "./fixtures/trivial/index.js")],
|
|
// splitting: true,
|
|
// });
|
|
// expect(x.outputs).toHaveLength(2);
|
|
// const [indexBlob, chunkBlob] = x.outputs;
|
|
|
|
// expect(indexBlob).toBeTruthy();
|
|
// expect(indexBlob.type).toBe("text/javascript;charset=utf-8");
|
|
// expect(indexBlob.size).toBeGreaterThan(1);
|
|
// expect(indexBlob.path).toBe("/index.js");
|
|
// expect(indexBlob.hash).toBeTruthy();
|
|
// expect(indexBlob.hash).toMatchSnapshot("hash index.js");
|
|
// expect(indexBlob.kind).toBe("entry-point");
|
|
// expect(indexBlob.loader).toBe("jsx");
|
|
// expect(indexBlob.sourcemap).toBe(null);
|
|
|
|
// expect(chunkBlob).toBeTruthy();
|
|
// expect(chunkBlob.type).toBe("text/javascript;charset=utf-8");
|
|
// expect(chunkBlob.size).toBeGreaterThan(1);
|
|
// expect(chunkBlob.path).toBe(`/foo-${chunkBlob.hash}.js`);
|
|
// expect(chunkBlob.hash).toBeTruthy();
|
|
// expect(chunkBlob.hash).toMatchSnapshot("hash foo.js");
|
|
// expect(chunkBlob.kind).toBe("chunk");
|
|
// expect(chunkBlob.loader).toBe("jsx");
|
|
// expect(chunkBlob.sourcemap).toBe(null);
|
|
// Bun.gc(true);
|
|
// });
|
|
|
|
test("new Response(BuildArtifact) sets content type", async () => {
|
|
const x = await Bun.build({
|
|
entrypoints: [join(import.meta.dir, "./fixtures/trivial/index.js")],
|
|
outdir: tempDirWithFiles("response-buildartifact", {}),
|
|
});
|
|
const response = new Response(x.outputs[0]);
|
|
expect(response.headers.get("content-type")).toBe("text/javascript;charset=utf-8");
|
|
expect(await response.text()).toMatchSnapshot("response text");
|
|
});
|
|
|
|
test.todo("new Response(BuildArtifact) sets etag", async () => {
|
|
const x = await Bun.build({
|
|
entrypoints: [join(import.meta.dir, "./fixtures/trivial/index.js")],
|
|
outdir: tempDirWithFiles("response-buildartifact-etag", {}),
|
|
});
|
|
const response = new Response(x.outputs[0]);
|
|
expect(response.headers.get("etag")).toBeTruthy();
|
|
expect(response.headers.get("etag")).toMatchSnapshot("content-etag");
|
|
});
|
|
|
|
// test("BuildArtifact with assets", async () => {
|
|
// const x = await Bun.build({
|
|
// entrypoints: [join(import.meta.dir, "./fixtures/with-assets/index.js")],
|
|
// loader: {
|
|
// ".blob": "file",
|
|
// ".png": "file",
|
|
// },
|
|
// });
|
|
// console.log(x);
|
|
// const [blob, asset] = x.outputs;
|
|
// expect(blob).toBeTruthy();
|
|
// expect(blob instanceof Blob).toBe(true);
|
|
// expect(blob.type).toBe("text/javascript;charset=utf-8");
|
|
// expect(blob.size).toBeGreaterThan(1);
|
|
// expect(blob.path).toBe("/index.js");
|
|
// expect(blob.hash).toBeTruthy();
|
|
// expect(blob.hash).toMatchSnapshot();
|
|
// expect(blob.kind).toBe("entry-point");
|
|
// expect(blob.loader).toBe("jsx");
|
|
// expect(blob.sourcemap).toBe(null);
|
|
// throw new Error("test was not fully written");
|
|
// });
|
|
|
|
test("errors are returned as an array", async () => {
|
|
const x = await buildNoThrow({
|
|
entrypoints: [join(import.meta.dir, "does-not-exist.ts")],
|
|
outdir: tempDirWithFiles("errors-are-returned-as-an-array", {}),
|
|
});
|
|
expect(x.success).toBe(false);
|
|
expect(x.logs).toHaveLength(1);
|
|
expect(x.logs[0].message).toMatch(/ModuleNotFound/);
|
|
expect(x.logs[0].name).toBe("BuildMessage");
|
|
expect(x.logs[0].position).toEqual(null);
|
|
});
|
|
|
|
test("warnings do not fail a build", async () => {
|
|
const x = await Bun.build({
|
|
entrypoints: [join(import.meta.dir, "./fixtures/jsx-warning/index.jsx")],
|
|
outdir: tempDirWithFiles("warnings-do-not-fail-a-build", {}),
|
|
});
|
|
expect(x.success).toBe(true);
|
|
expect(x.logs).toHaveLength(1);
|
|
expect(x.logs[0].message).toBe(
|
|
'"key" prop after a {...spread} is deprecated in JSX. Falling back to classic runtime.',
|
|
);
|
|
expect(x.logs[0].name).toBe("BuildMessage");
|
|
expect(x.logs[0].position).toBeTruthy();
|
|
});
|
|
|
|
test("module() throws error", async () => {
|
|
expect(() =>
|
|
Bun.build({
|
|
entrypoints: [join(import.meta.dir, "./fixtures/trivial/bundle-ws.ts")],
|
|
plugins: [
|
|
{
|
|
name: "test",
|
|
setup: b => {
|
|
b.module("ad", () => {
|
|
return {
|
|
exports: {
|
|
hello: "world",
|
|
},
|
|
loader: "object",
|
|
};
|
|
});
|
|
},
|
|
},
|
|
],
|
|
}),
|
|
).toThrow();
|
|
});
|
|
|
|
test("non-object plugins throw invalid argument errors", () => {
|
|
for (const plugin of [null, undefined, 1, "hello", true, false, Symbol.for("hello")]) {
|
|
expect(() => {
|
|
Bun.build({
|
|
entrypoints: [join(import.meta.dir, "./fixtures/trivial/bundle-ws.ts")],
|
|
plugins: [
|
|
// @ts-expect-error
|
|
plugin,
|
|
],
|
|
});
|
|
}).toThrow("Expected plugin to be an object");
|
|
}
|
|
});
|
|
|
|
test("hash considers cross chunk imports", async () => {
|
|
Bun.gc(true);
|
|
const fixture = tempDirWithFiles("build-hash-cross-chunk-imports", {
|
|
"entry1.ts": `
|
|
import { bar } from './bar'
|
|
export const entry1 = () => {
|
|
console.log('FOO')
|
|
bar()
|
|
}
|
|
`,
|
|
"entry2.ts": `
|
|
import { bar } from './bar'
|
|
export const entry1 = () => {
|
|
console.log('FOO')
|
|
bar()
|
|
}
|
|
`,
|
|
"bar.ts": `
|
|
export const bar = () => {
|
|
console.log('BAR')
|
|
}
|
|
`,
|
|
});
|
|
const first = await Bun.build({
|
|
entrypoints: [join(fixture, "entry1.ts"), join(fixture, "entry2.ts")],
|
|
outdir: join(fixture, "out"),
|
|
target: "browser",
|
|
splitting: true,
|
|
minify: false,
|
|
naming: "[dir]/[name]-[hash].[ext]",
|
|
});
|
|
if (!first.success) throw new AggregateError(first.logs);
|
|
expect(first.outputs.length).toBe(3);
|
|
|
|
writeFileSync(join(fixture, "bar.ts"), readFileSync(join(fixture, "bar.ts"), "utf8").replace("BAR", "BAZ"));
|
|
|
|
const second = await Bun.build({
|
|
entrypoints: [join(fixture, "entry1.ts"), join(fixture, "entry2.ts")],
|
|
outdir: join(fixture, "out2"),
|
|
target: "browser",
|
|
splitting: true,
|
|
minify: false,
|
|
naming: "[dir]/[name]-[hash].[ext]",
|
|
});
|
|
if (!second.success) throw new AggregateError(second.logs);
|
|
expect(second.outputs.length).toBe(3);
|
|
|
|
const totalUniqueHashes = new Set();
|
|
const allFiles = [...first.outputs, ...second.outputs];
|
|
for (const out of allFiles) totalUniqueHashes.add(out.hash);
|
|
|
|
expect(
|
|
totalUniqueHashes.size,
|
|
"number of unique hashes should be 6: three per bundle. the changed foo.ts affects all chunks",
|
|
).toBe(6);
|
|
|
|
// ensure that the hashes are in the path
|
|
for (const out of allFiles) {
|
|
expect(out.path).toInclude(out.hash!);
|
|
}
|
|
|
|
Bun.gc(true);
|
|
});
|
|
|
|
test("ignoreDCEAnnotations works", async () => {
|
|
const fixture = tempDirWithFiles("build-ignore-dce-annotations", {
|
|
"package.json": `{}`,
|
|
"entry.ts": `
|
|
/* @__PURE__ */ console.log(1)
|
|
`,
|
|
});
|
|
|
|
const bundle = await Bun.build({
|
|
entrypoints: [join(fixture, "entry.ts")],
|
|
ignoreDCEAnnotations: true,
|
|
minify: true,
|
|
outdir: path.join(fixture, "out"),
|
|
});
|
|
if (!bundle.success) throw new AggregateError(bundle.logs);
|
|
|
|
expect(await bundle.outputs[0].text()).toBe("console.log(1);\n");
|
|
});
|
|
|
|
test("emitDCEAnnotations works", async () => {
|
|
const fixture = tempDirWithFiles("build-emit-dce-annotations", {
|
|
"package.json": `{}`,
|
|
"entry.ts": `
|
|
export const OUT = /* @__PURE__ */ console.log(1)
|
|
`,
|
|
});
|
|
|
|
const bundle = await Bun.build({
|
|
entrypoints: [join(fixture, "entry.ts")],
|
|
emitDCEAnnotations: true,
|
|
minify: true,
|
|
outdir: path.join(fixture, "out"),
|
|
});
|
|
if (!bundle.success) throw new AggregateError(bundle.logs);
|
|
|
|
expect(await bundle.outputs[0].text()).toBe("var o=/*@__PURE__*/console.log(1);export{o as OUT};\n");
|
|
});
|
|
|
|
test("you can write onLoad and onResolve plugins using the 'html' loader, and it includes script and link tags as bundled entrypoints", async () => {
|
|
const fixture = tempDirWithFiles("build-html-plugins", {
|
|
"index.html": `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<link rel="stylesheet" href="./style.css">
|
|
<script src="./script.js"></script>
|
|
</head>
|
|
</html>
|
|
`,
|
|
"style.css": ".foo { color: red; }",
|
|
|
|
// Check we actually do bundle the script
|
|
"script.js": "console.log(1 + 2)",
|
|
});
|
|
|
|
let onLoadCalled = false;
|
|
let onResolveCalled = false;
|
|
|
|
const build = await Bun.build({
|
|
entrypoints: [join(fixture, "index.html")],
|
|
minify: {
|
|
syntax: true,
|
|
},
|
|
plugins: [
|
|
{
|
|
name: "test-plugin",
|
|
setup(build) {
|
|
build.onLoad({ filter: /\.html$/ }, async args => {
|
|
onLoadCalled = true;
|
|
const contents = await Bun.file(args.path).text();
|
|
return {
|
|
contents: contents.replace("</head>", "<meta name='injected-by-plugin' content='true'></head>"),
|
|
loader: "html",
|
|
};
|
|
});
|
|
|
|
build.onResolve({ filter: /\.(js|css)$/ }, args => {
|
|
onResolveCalled = true;
|
|
return {
|
|
path: join(fixture, args.path),
|
|
namespace: "file",
|
|
};
|
|
});
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(build.success).toBe(true);
|
|
expect(onLoadCalled).toBe(true);
|
|
expect(onResolveCalled).toBe(true);
|
|
|
|
// Should have 3 outputs - HTML, JS and CSS
|
|
expect(build.outputs).toHaveLength(3);
|
|
|
|
// Verify we have one of each type
|
|
const types = build.outputs.map(o => o.type);
|
|
expect(types).toContain("text/html;charset=utf-8");
|
|
expect(types).toContain("text/javascript;charset=utf-8");
|
|
expect(types).toContain("text/css;charset=utf-8");
|
|
|
|
// Verify the JS output contains the __dirname
|
|
const js = build.outputs.find(o => o.type === "text/javascript;charset=utf-8");
|
|
expect(await js?.text()).toContain("console.log(3)");
|
|
|
|
// Verify our plugin modified the HTML
|
|
const html = build.outputs.find(o => o.type === "text/html;charset=utf-8");
|
|
expect(await html?.text()).toContain("<meta name='injected-by-plugin' content='true'>");
|
|
});
|
|
});
|
|
|
|
test("macro with nested object", async () => {
|
|
const dir = tempDirWithFilesAnon({
|
|
"index.ts": `
|
|
import { testMacro } from "./macro" assert { type: "macro" };
|
|
|
|
export const testConfig = testMacro({
|
|
borderRadius: {
|
|
1: "4px",
|
|
2: "8px",
|
|
},
|
|
});
|
|
`,
|
|
"macro.ts": `
|
|
export function testMacro(val: any) {
|
|
return val;
|
|
}
|
|
`,
|
|
});
|
|
|
|
const build = await Bun.build({
|
|
entrypoints: [join(dir, "index.ts")],
|
|
minify: true,
|
|
});
|
|
|
|
expect(build.outputs).toHaveLength(1);
|
|
expect(build.outputs[0].kind).toBe("entry-point");
|
|
expect(await build.outputs[0].text()).toEqualIgnoringWhitespace(
|
|
`var t={borderRadius:{"1":"4px","2":"8px"}};export{t as testConfig};\n`,
|
|
);
|
|
});
|
|
|
|
// Since NODE_PATH has to be set, we need to run this test outside the bundler tests.
|
|
test("regression/NODE_PATHBuild api", async () => {
|
|
const dir = tempDirWithFiles("node-path-build", {
|
|
"entry.js": `
|
|
import MyClass from 'MyClass';
|
|
console.log(new MyClass().constructor.name);
|
|
`,
|
|
"src/MyClass.js": `
|
|
export default class MyClass {}
|
|
`,
|
|
"build.js": `
|
|
import { join } from "path";
|
|
|
|
const build = await Bun.build({
|
|
entrypoints: [join(import.meta.dir, "entry.js")],
|
|
outdir: join(import.meta.dir, "out"),
|
|
});
|
|
|
|
if (!build.success) {
|
|
console.error("Build failed:", build.logs);
|
|
process.exit(1);
|
|
}
|
|
|
|
// Run the built file
|
|
const runProc = Bun.spawn({
|
|
cmd: [process.argv[0], join(import.meta.dir, "out", "entry.js")],
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
await runProc.exited;
|
|
const runOutput = await new Response(runProc.stdout).text();
|
|
const runError = await new Response(runProc.stderr).text();
|
|
|
|
if (runError) {
|
|
console.error("Run error:", runError);
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(runOutput.trim());
|
|
|
|
`,
|
|
});
|
|
|
|
// Run the build script with NODE_PATH set
|
|
const proc = Bun.spawn({
|
|
cmd: [bunExe(), join(dir, "build.js")],
|
|
env: {
|
|
...bunEnv,
|
|
NODE_PATH: join(dir, "src"),
|
|
},
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
cwd: dir,
|
|
});
|
|
|
|
await proc.exited;
|
|
const output = await proc.stdout.text();
|
|
const error = await proc.stderr.text();
|
|
|
|
expect(error).toBe("");
|
|
expect(output.trim()).toBe("MyClass");
|
|
});
|
|
|
|
test("regression/GlobalThis", async () => {
|
|
const dir = tempDirWithFiles("global-this-regression", {
|
|
"entry.js": `
|
|
function identity(x) {
|
|
return x;
|
|
}
|
|
import * as mod1 from 'assert';
|
|
identity(mod1);
|
|
import * as mod2 from 'buffer';
|
|
identity(mod2);
|
|
import * as mod3 from 'console';
|
|
identity(mod3);
|
|
import * as mod4 from 'constants';
|
|
identity(mod4);
|
|
import * as mod5 from 'crypto';
|
|
identity(mod5);
|
|
import * as mod6 from 'domain';
|
|
identity(mod6);
|
|
import * as mod7 from 'events';
|
|
identity(mod7);
|
|
import * as mod8 from 'http';
|
|
identity(mod8);
|
|
import * as mod9 from 'https';
|
|
identity(mod9);
|
|
import * as mod10 from 'net';
|
|
identity(mod10);
|
|
import * as mod11 from 'os';
|
|
identity(mod11);
|
|
import * as mod12 from 'path';
|
|
identity(mod12);
|
|
import * as mod13 from 'process';
|
|
identity(mod13);
|
|
import * as mod14 from 'punycode';
|
|
identity(mod14);
|
|
import * as mod15 from 'stream';
|
|
identity(mod15);
|
|
import * as mod16 from 'string_decoder';
|
|
identity(mod16);
|
|
import * as mod17 from 'sys';
|
|
identity(mod17);
|
|
import * as mod18 from 'timers';
|
|
identity(mod18);
|
|
import * as mod20 from 'tty';
|
|
identity(mod20);
|
|
import * as mod21 from 'url';
|
|
identity(mod21);
|
|
import * as mod22 from 'util';
|
|
identity(mod22);
|
|
import * as mod23 from 'zlib';
|
|
identity(mod23);
|
|
`,
|
|
});
|
|
|
|
const build = await Bun.build({
|
|
entrypoints: [join(dir, "entry.js")],
|
|
target: "browser",
|
|
});
|
|
|
|
expect(build.success).toBe(true);
|
|
const text = await build.outputs[0].text();
|
|
expect(text).not.toContain("process.env.");
|
|
expect(text).not.toContain(" global.");
|
|
expect(text).toContain(" globalThis.");
|
|
});
|
|
|
|
describe("sourcemap boolean values", () => {
|
|
test("sourcemap: true should work (boolean)", async () => {
|
|
const dir = tempDirWithFiles("sourcemap-true-boolean", {
|
|
"index.js": `console.log("hello");`,
|
|
});
|
|
|
|
const build = await Bun.build({
|
|
entrypoints: [join(dir, "index.js")],
|
|
sourcemap: true,
|
|
});
|
|
|
|
expect(build.success).toBe(true);
|
|
expect(build.outputs).toHaveLength(1);
|
|
expect(build.outputs[0].kind).toBe("entry-point");
|
|
|
|
const output = await build.outputs[0].text();
|
|
expect(output).toContain("//# sourceMappingURL=data:application/json;base64,");
|
|
});
|
|
|
|
test("sourcemap: false should work (boolean)", async () => {
|
|
const dir = tempDirWithFiles("sourcemap-false-boolean", {
|
|
"index.js": `console.log("hello");`,
|
|
});
|
|
|
|
const build = await Bun.build({
|
|
entrypoints: [join(dir, "index.js")],
|
|
sourcemap: false,
|
|
});
|
|
|
|
expect(build.success).toBe(true);
|
|
expect(build.outputs).toHaveLength(1);
|
|
expect(build.outputs[0].kind).toBe("entry-point");
|
|
|
|
const output = await build.outputs[0].text();
|
|
expect(output).not.toContain("//# sourceMappingURL=");
|
|
});
|
|
|
|
test("sourcemap: true with outdir should create linked sourcemap", async () => {
|
|
const dir = tempDirWithFiles("sourcemap-true-outdir", {
|
|
"index.js": `console.log("hello");`,
|
|
});
|
|
|
|
const build = await Bun.build({
|
|
entrypoints: [join(dir, "index.js")],
|
|
outdir: join(dir, "out"),
|
|
sourcemap: true,
|
|
});
|
|
|
|
expect(build.success).toBe(true);
|
|
expect(build.outputs).toHaveLength(2);
|
|
|
|
const jsOutput = build.outputs.find(o => o.kind === "entry-point");
|
|
const mapOutput = build.outputs.find(o => o.kind === "sourcemap");
|
|
|
|
expect(jsOutput).toBeTruthy();
|
|
expect(mapOutput).toBeTruthy();
|
|
expect(jsOutput!.sourcemap).toBe(mapOutput!);
|
|
|
|
const jsText = await jsOutput!.text();
|
|
expect(jsText).toContain("//# sourceMappingURL=index.js.map");
|
|
});
|
|
});
|
|
|
|
const originalCwd = process.cwd() + "";
|
|
|
|
describe("tsconfig option", () => {
|
|
afterEach(() => {
|
|
process.chdir(originalCwd);
|
|
});
|
|
|
|
test("should resolve path mappings", async () => {
|
|
const dir = tempDirWithFiles("tsconfig-api-basic", {
|
|
"tsconfig.json": `{
|
|
"compilerOptions": {
|
|
"paths": {
|
|
"@/*": ["./src/*"]
|
|
}
|
|
}
|
|
}`,
|
|
"src/utils.ts": `export const greeting = "Hello World";`,
|
|
"index.ts": `import { greeting } from "@/utils";
|
|
export { greeting };`,
|
|
});
|
|
|
|
try {
|
|
process.chdir(dir);
|
|
const result = await Bun.build({
|
|
entrypoints: ["./index.ts"],
|
|
tsconfig: "./tsconfig.json",
|
|
});
|
|
expect(result.success).toBe(true);
|
|
expect(result.outputs).toHaveLength(1);
|
|
const output = await result.outputs[0].text();
|
|
expect(output).toContain("Hello World");
|
|
} finally {
|
|
process.chdir(originalCwd);
|
|
}
|
|
});
|
|
|
|
test("should work from nested directories", async () => {
|
|
const dir = tempDirWithFiles("tsconfig-api-nested", {
|
|
"tsconfig.json": `{
|
|
"compilerOptions": {
|
|
"paths": {
|
|
"@/*": ["./src/*"]
|
|
}
|
|
}
|
|
}`,
|
|
"src/utils.ts": `export const greeting = "Hello World";`,
|
|
"src/nested/index.ts": `import { greeting } from "@/utils";
|
|
export { greeting };`,
|
|
});
|
|
|
|
try {
|
|
process.chdir(join(dir, "src/nested"));
|
|
const result = await Bun.build({
|
|
entrypoints: ["./index.ts"],
|
|
tsconfig: "../../tsconfig.json",
|
|
});
|
|
expect(result.success).toBe(true);
|
|
expect(result.outputs).toHaveLength(1);
|
|
const output = await result.outputs[0].text();
|
|
expect(output).toContain("Hello World");
|
|
} finally {
|
|
process.chdir(originalCwd);
|
|
}
|
|
});
|
|
|
|
test("should handle relative tsconfig paths", async () => {
|
|
const dir = tempDirWithFiles("tsconfig-api-relative", {
|
|
"tsconfig.json": `{
|
|
"compilerOptions": {
|
|
"baseUrl": ".",
|
|
"paths": {
|
|
"@/*": ["src/*"]
|
|
}
|
|
}
|
|
}`,
|
|
"configs/build-tsconfig.json": `{
|
|
"extends": "../tsconfig.json",
|
|
"compilerOptions": {
|
|
"baseUrl": ".."
|
|
}
|
|
}`,
|
|
"src/utils.ts": `export const greeting = "Hello World";`,
|
|
"index.ts": `import { greeting } from "@/utils";
|
|
export { greeting };`,
|
|
});
|
|
|
|
try {
|
|
process.chdir(dir);
|
|
const result = await Bun.build({
|
|
entrypoints: ["./index.ts"],
|
|
tsconfig: "./configs/build-tsconfig.json",
|
|
});
|
|
expect(result.success).toBe(true);
|
|
expect(result.outputs).toHaveLength(1);
|
|
const output = await result.outputs[0].text();
|
|
expect(output).toContain("Hello World");
|
|
} finally {
|
|
process.chdir(originalCwd);
|
|
}
|
|
});
|
|
|
|
test("onEnd fires before promise resolves with throw: true", async () => {
|
|
const dir = tempDirWithFiles("onend-throwonerror-true", {
|
|
"index.ts": `
|
|
// This will cause a build error
|
|
import { missing } from "./does-not-exist";
|
|
console.log(missing);
|
|
`,
|
|
});
|
|
|
|
let onEndCalled = false;
|
|
let onEndCalledBeforeReject = false;
|
|
let promiseRejected = false;
|
|
|
|
try {
|
|
await Bun.build({
|
|
entrypoints: [join(dir, "index.ts")],
|
|
throw: true,
|
|
plugins: [
|
|
{
|
|
name: "test-plugin",
|
|
setup(builder) {
|
|
builder.onEnd(result => {
|
|
onEndCalled = true;
|
|
onEndCalledBeforeReject = !promiseRejected;
|
|
// Result should contain error information
|
|
expect(result.success).toBe(false);
|
|
expect(result.logs).toBeDefined();
|
|
expect(result.logs.length).toBeGreaterThan(0);
|
|
});
|
|
},
|
|
},
|
|
],
|
|
});
|
|
// Should not reach here
|
|
expect(false).toBe(true);
|
|
} catch (error) {
|
|
promiseRejected = true;
|
|
// Verify onEnd was called before promise rejected
|
|
expect(onEndCalled).toBe(true);
|
|
expect(onEndCalledBeforeReject).toBe(true);
|
|
}
|
|
});
|
|
|
|
test("onEnd fires before promise resolves with throw: false", async () => {
|
|
const dir = tempDirWithFiles("onend-throwonerror-false", {
|
|
"index.ts": `
|
|
// This will cause a build error
|
|
import { missing } from "./does-not-exist";
|
|
console.log(missing);
|
|
`,
|
|
});
|
|
|
|
let onEndCalled = false;
|
|
let onEndCalledBeforeResolve = false;
|
|
let promiseResolved = false;
|
|
|
|
const result = await Bun.build({
|
|
entrypoints: [join(dir, "index.ts")],
|
|
throw: false,
|
|
plugins: [
|
|
{
|
|
name: "test-plugin",
|
|
setup(builder) {
|
|
builder.onEnd(result => {
|
|
onEndCalled = true;
|
|
onEndCalledBeforeResolve = !promiseResolved;
|
|
// Result should contain error information
|
|
expect(result.success).toBe(false);
|
|
expect(result.logs).toBeDefined();
|
|
expect(result.logs.length).toBeGreaterThan(0);
|
|
});
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
promiseResolved = true;
|
|
|
|
// Verify onEnd was called before promise resolved
|
|
expect(onEndCalled).toBe(true);
|
|
expect(onEndCalledBeforeResolve).toBe(true);
|
|
expect(result.success).toBe(false);
|
|
expect(result.logs.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("onEnd always fires on successful build", async () => {
|
|
const dir = tempDirWithFiles("onend-success", {
|
|
"index.ts": `
|
|
export const message = "Build successful";
|
|
console.log(message);
|
|
`,
|
|
});
|
|
|
|
let onEndCalled = false;
|
|
let onEndCalledBeforeResolve = false;
|
|
let promiseResolved = false;
|
|
|
|
const result = await Bun.build({
|
|
entrypoints: [join(dir, "index.ts")],
|
|
throw: true, // Should not matter for successful build
|
|
plugins: [
|
|
{
|
|
name: "test-plugin",
|
|
setup(builder) {
|
|
builder.onEnd(result => {
|
|
onEndCalled = true;
|
|
onEndCalledBeforeResolve = !promiseResolved;
|
|
// Result should indicate success
|
|
expect(result.success).toBe(true);
|
|
expect(result.outputs).toBeDefined();
|
|
expect(result.outputs.length).toBeGreaterThan(0);
|
|
});
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
promiseResolved = true;
|
|
|
|
// Verify onEnd was called before promise resolved
|
|
expect(onEndCalled).toBe(true);
|
|
expect(onEndCalledBeforeResolve).toBe(true);
|
|
expect(result.success).toBe(true);
|
|
const output = await result.outputs[0].text();
|
|
expect(output).toContain("Build successful");
|
|
});
|
|
|
|
test("multiple onEnd callbacks fire in order before promise settles", async () => {
|
|
const dir = tempDirWithFiles("onend-multiple", {
|
|
"index.ts": `
|
|
// This will cause a build error
|
|
import { missing } from "./not-found";
|
|
`,
|
|
});
|
|
|
|
const callOrder: string[] = [];
|
|
let promiseSettled = false;
|
|
|
|
const result = await Bun.build({
|
|
entrypoints: [join(dir, "index.ts")],
|
|
throw: false,
|
|
plugins: [
|
|
{
|
|
name: "plugin-1",
|
|
setup(builder) {
|
|
builder.onEnd(() => {
|
|
callOrder.push("first");
|
|
expect(promiseSettled).toBe(false);
|
|
});
|
|
},
|
|
},
|
|
{
|
|
name: "plugin-2",
|
|
setup(builder) {
|
|
builder.onEnd(() => {
|
|
callOrder.push("second");
|
|
expect(promiseSettled).toBe(false);
|
|
});
|
|
},
|
|
},
|
|
{
|
|
name: "plugin-3",
|
|
setup(builder) {
|
|
builder.onEnd(() => {
|
|
callOrder.push("third");
|
|
expect(promiseSettled).toBe(false);
|
|
});
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
promiseSettled = true;
|
|
|
|
// All callbacks should have fired in order before promise resolved
|
|
expect(callOrder).toEqual(["first", "second", "third"]);
|
|
// The build actually succeeds because the import is being resolved to nothing
|
|
// What matters is that callbacks fired before promise settled
|
|
expect(result.success).toBeDefined();
|
|
});
|
|
});
|