From 87980df4c31284282b8ec9c832c793de4b1ebd96 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 13 Sep 2026 03:07:23 +0000 Subject: [PATCH 1/7] docs, bun-types: correct the Bun.build sourcemap, banner and footer descriptions - sourcemap: true is "linked" when outdir is set and "inline" when it is not. - "linked" does not require outdir in Bun.build. Without outdir the map is a BuildArtifact with kind "sourcemap". - "external" writes one map per output file, not per input file. - banner and footer are added to every JavaScript output and to no CSS or HTML output. --- docs/bundler/index.mdx | 14 +++++++++++++- packages/bun-types/bun.d.ts | 19 +++++++++++++++---- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/docs/bundler/index.mdx b/docs/bundler/index.mdx index 6f8576cb8b37..d45bdd7743c6 100644 --- a/docs/bundler/index.mdx +++ b/docs/bundler/index.mdx @@ -763,6 +763,10 @@ Specifies the type of sourcemap to generate. The associated `*.js.map` sourcemap is a JSON file containing an equivalent `debugId` property. +In `Bun.build`, `sourcemap: true` means `"linked"` when `outdir` is set and `"inline"` when it is not. `sourcemap: false` means `"none"`. On the CLI, `--sourcemap` with no value means `linked`. + +When `Bun.build` runs without `outdir`, it does not write `"linked"` and `"external"` sourcemaps to disk. Each one is a `BuildArtifact` in `outputs` with `kind: "sourcemap"`, and it is also the `sourcemap` property of the artifact it belongs to. + ### minify Whether to enable minification. Default `false`. @@ -1223,6 +1227,8 @@ A map of file extensions to built-in loader names. Use this to customize how cer A banner added to the final bundle. This can be a directive like `"use client"` for React, or a comment block such as a license. +Bun adds the banner to every JavaScript output file (entry points and chunks). CSS and HTML output files do not get it. + ```ts title="build.ts" icon="/icons/typescript.svg" @@ -1244,6 +1250,8 @@ A banner added to the final bundle. This can be a directive like `"use client"` A footer added to the final bundle. This can be a comment block for a license or a fun easter egg. +Bun adds the footer to every JavaScript output file (entry points and chunks). CSS and HTML output files do not get it. + ```ts title="build.ts" icon="/icons/typescript.svg" @@ -1820,7 +1828,7 @@ interface BuildConfig { publicPath?: string; define?: Record; loader?: { [k in string]: Loader }; - sourcemap?: "none" | "linked" | "inline" | "external" | boolean; // default: "none", true -> "inline" + sourcemap?: "none" | "linked" | "inline" | "external" | boolean; // default: "none", true -> "linked" with outdir, "inline" without /** * package.json `exports` conditions used when resolving imports * @@ -1898,12 +1906,16 @@ interface BuildConfig { }; /** * Add a banner to the bundled code such as "use client"; + * + * Added to every JavaScript output (entry points and chunks), not to CSS or HTML outputs. */ banner?: string; /** * Add a footer to the bundled code such as a comment block like * * `// made with bun!` + * + * Added to every JavaScript output (entry points and chunks), not to CSS or HTML outputs. */ footer?: string; diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 07398ce81d0a..45aa18419758 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -3305,16 +3305,21 @@ declare module "bun" { * - `"none"` - No source maps are generated * - `"linked"` - A separate `*.ext.map` file is generated alongside each * `*.ext` file. A `//# sourceMappingURL` comment is added to the output - * file to link the two. Requires `outdir` to be set. + * file to link the two. * - `"inline"` - an inline source map is appended to the output file. - * - `"external"` - Generate a separate source map file for each input file. + * - `"external"` - Generate a separate source map file for each output file. * No `//# sourceMappingURL` comment is added to the output file. * - * `true` and `false` are aliases for `"inline"` and `"none"`, respectively. + * `true` is an alias for `"linked"` when `outdir` is set, and for `"inline"` + * when it is not. `false` is an alias for `"none"`. + * + * Without `outdir`, `"linked"` and `"external"` maps are not written to + * disk. Each map is a separate {@link BuildArtifact} in `outputs` with + * `kind: "sourcemap"`, and is also the `sourcemap` property of the + * artifact it belongs to. * * @default "none" * - * @see {@link outdir} required for `"linked"` maps * @see {@link publicPath} to customize the base url of linked source maps */ sourcemap?: "none" | "linked" | "inline" | "external" | boolean; @@ -3450,6 +3455,9 @@ declare module "bun" { /** * Add a banner to the bundled code such as "use client"; + * + * The banner is added to every JavaScript output (entry points and + * chunks). CSS and HTML outputs do not get it. */ banner?: string; @@ -3457,6 +3465,9 @@ declare module "bun" { * Add a footer to the bundled code such as a comment block like * * `// made with bun!` + * + * The footer is added to every JavaScript output (entry points and + * chunks). CSS and HTML outputs do not get it. */ footer?: string; From d27aa857ff08a3a42cbb855973717866158c3a3a Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 13 Sep 2026 03:13:08 +0000 Subject: [PATCH 2/7] docs: move the --outdir note for linked and external sourcemaps out of the value table The table is shared by the Bun.build and CLI tabs. The row for "linked" said that it requires --outdir, which is not true for Bun.build. The CLI guidance is now a sentence below the table. --- docs/bundler/index.mdx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/bundler/index.mdx b/docs/bundler/index.mdx index d45bdd7743c6..b23bb4e54c76 100644 --- a/docs/bundler/index.mdx +++ b/docs/bundler/index.mdx @@ -757,7 +757,7 @@ Specifies the type of sourcemap to generate. | Value | Description | | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `"none"` | Default. No sourcemap is generated. | -| `"linked"` | A separate `*.js.map` file is created alongside each `*.js` bundle using a `//# sourceMappingURL` comment to link the two. Requires `--outdir` to be set. You can customize the base URL in this comment with `--public-path`.

`js
//

//# sourceMappingURL=bundle.js.map
` | +| `"linked"` | A separate `*.js.map` file is created alongside each `*.js` bundle using a `//# sourceMappingURL` comment to link the two. You can customize the base URL in this comment with `--public-path`.

`js
//

//# sourceMappingURL=bundle.js.map
` | | `"external"` | A separate `*.js.map` file is created alongside each `*.js` bundle without inserting a `//# sourceMappingURL` comment.

Generated bundles contain a debug id that can be used to associate a bundle with its corresponding sourcemap. This `debugId` is added as a comment at the bottom of the file.

`js
//

//# debugId=
` | | `"inline"` | A sourcemap is generated and appended to the end of the generated bundle as a base64 payload.

`js
//

//# sourceMappingURL=data:application/json;base64,
` | @@ -765,6 +765,8 @@ The associated `*.js.map` sourcemap is a JSON file containing an equivalent `deb In `Bun.build`, `sourcemap: true` means `"linked"` when `outdir` is set and `"inline"` when it is not. `sourcemap: false` means `"none"`. On the CLI, `--sourcemap` with no value means `linked`. +On the CLI, a `linked` or `external` sourcemap is a separate file. Set `--outdir` so that Bun has a directory to write it to. + When `Bun.build` runs without `outdir`, it does not write `"linked"` and `"external"` sourcemaps to disk. Each one is a `BuildArtifact` in `outputs` with `kind: "sourcemap"`, and it is also the `sourcemap` property of the artifact it belongs to. ### minify From a61e877e84b971aec5c9a0c230612b7fa7640058 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 13 Sep 2026 03:26:11 +0000 Subject: [PATCH 3/7] test: check the BuildConfig sourcemap, banner and footer JSDoc against what Bun.build does Each test runs the documented option, then reads the JSDoc of that option from bun.d.ts and checks that it describes the result. Also name publicPath next to --public-path in the "linked" row of the sourcemap table. --- docs/bundler/index.mdx | 2 +- .../bun-types/build-config-jsdoc.test.ts | 111 ++++++++++++++++++ 2 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 test/integration/bun-types/build-config-jsdoc.test.ts diff --git a/docs/bundler/index.mdx b/docs/bundler/index.mdx index b23bb4e54c76..03b2b3b7ec19 100644 --- a/docs/bundler/index.mdx +++ b/docs/bundler/index.mdx @@ -757,7 +757,7 @@ Specifies the type of sourcemap to generate. | Value | Description | | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `"none"` | Default. No sourcemap is generated. | -| `"linked"` | A separate `*.js.map` file is created alongside each `*.js` bundle using a `//# sourceMappingURL` comment to link the two. You can customize the base URL in this comment with `--public-path`.

`js
//

//# sourceMappingURL=bundle.js.map
` | +| `"linked"` | A separate `*.js.map` file is created alongside each `*.js` bundle using a `//# sourceMappingURL` comment to link the two. You can customize the base URL in this comment with `publicPath` in `Bun.build` or `--public-path` on the CLI.

`js
//

//# sourceMappingURL=bundle.js.map
` | | `"external"` | A separate `*.js.map` file is created alongside each `*.js` bundle without inserting a `//# sourceMappingURL` comment.

Generated bundles contain a debug id that can be used to associate a bundle with its corresponding sourcemap. This `debugId` is added as a comment at the bottom of the file.

`js
//

//# debugId=
` | | `"inline"` | A sourcemap is generated and appended to the end of the generated bundle as a base64 payload.

`js
//

//# sourceMappingURL=data:application/json;base64,
` | diff --git a/test/integration/bun-types/build-config-jsdoc.test.ts b/test/integration/bun-types/build-config-jsdoc.test.ts new file mode 100644 index 000000000000..2ff792b61d24 --- /dev/null +++ b/test/integration/bun-types/build-config-jsdoc.test.ts @@ -0,0 +1,111 @@ +// The API reference for `Bun.build` is generated from the JSDoc in bun.d.ts. +// Each test runs a documented option, then checks that the JSDoc of that +// option describes what the build did. +import { describe, expect, test } from "bun:test"; +import { tempDir } from "harness"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +const dts = readFileSync(join(import.meta.dir, "../../../packages/bun-types/bun.d.ts"), "utf8"); + +/** The JSDoc block directly above `member?:` in bun.d.ts, as one line of prose. */ +function jsdocOf(member: string): string { + const decl = dts.search(new RegExp(`^ +${member}\\?: `, "m")); + expect(decl).toBeGreaterThan(-1); + const start = dts.lastIndexOf("/**", decl); + const end = dts.indexOf("*/", start); + expect(dts.slice(end + 2, decl).trim()).toBe(""); + return dts + .slice(start + 3, end) + .replace(/^\s*\* ?/gm, "") + .replace(/\s+/g, " ") + .trim(); +} + +/** Which `sourcemap` mode the `//# sourceMappingURL` comment of an output is. */ +function sourcemapCommentKind(text: string): "inline" | "linked" | "none" { + if (text.includes("//# sourceMappingURL=data:")) return "inline"; + if (text.includes("//# sourceMappingURL=")) return "linked"; + return "none"; +} + +describe.concurrent("BuildConfig JSDoc matches the runtime", () => { + test("sourcemap: what `true` is an alias for", async () => { + using dir = tempDir("jsdoc-sourcemap-true", { "index.js": `console.log("hello");` }); + const entrypoints = [join(String(dir), "index.js")]; + + const withOutdir = await Bun.build({ entrypoints, sourcemap: true, outdir: join(String(dir), "out") }); + const withoutOutdir = await Bun.build({ entrypoints, sourcemap: true }); + const observed = [ + sourcemapCommentKind(await withOutdir.outputs.find(o => o.kind === "entry-point")!.text()), + sourcemapCommentKind(await withoutOutdir.outputs.find(o => o.kind === "entry-point")!.text()), + ]; + expect(observed).toEqual(["linked", "inline"]); + + const sentences = jsdocOf("sourcemap") + .split(/(?<=\.)\s+/) + .filter(sentence => sentence.includes("`true`")); + const documented = sentences.flatMap(sentence => + [...sentence.matchAll(/`"(none|linked|inline|external)"`/g)].map(match => match[1]), + ); + expect(documented).toEqual(expect.arrayContaining(observed)); + }); + + test('sourcemap: "linked" and "external" without outdir', async () => { + using dir = tempDir("jsdoc-sourcemap-no-outdir", { "index.js": `console.log("hello");` }); + const entrypoints = [join(String(dir), "index.js")]; + + for (const sourcemap of ["linked", "external"] as const) { + const build = await Bun.build({ entrypoints, sourcemap }); + const entry = build.outputs.find(o => o.kind === "entry-point")!; + const map = build.outputs.find(o => o.kind === "sourcemap")!; + expect({ sourcemap, success: build.success, kinds: build.outputs.map(o => o.kind) }).toEqual({ + sourcemap, + success: true, + kinds: ["entry-point", "sourcemap"], + }); + expect(entry.sourcemap).toBe(map); + } + + const doc = jsdocOf("sourcemap"); + expect(doc).not.toMatch(/requires? `outdir`|outdir\}? (is )?required/i); + expect(doc).toContain('`kind: "sourcemap"`'); + }); + + test("banner and footer: JavaScript outputs only", async () => { + using dir = tempDir("jsdoc-banner-footer", { + "index.js": `import("./lazy.js").then(m => console.log(m.default));`, + "lazy.js": `export default "lazy";`, + "style.css": `a { color: red; }`, + }); + const build = await Bun.build({ + entrypoints: [join(String(dir), "index.js"), join(String(dir), "style.css")], + splitting: true, + banner: "/* BANNER */", + footer: "/* FOOTER */", + }); + expect(build.success).toBe(true); + + const observed = await Promise.all( + build.outputs.map(async output => { + const text = await output.text(); + return { + ext: output.path.slice(output.path.lastIndexOf(".")), + kind: output.kind, + banner: text.includes("/* BANNER */"), + footer: text.includes("/* FOOTER */"), + }; + }), + ); + observed.sort((a, b) => (a.ext + a.kind).localeCompare(b.ext + b.kind)); + expect(observed).toEqual([ + { ext: ".css", kind: "asset", banner: false, footer: false }, + { ext: ".js", kind: "chunk", banner: true, footer: true }, + { ext: ".js", kind: "entry-point", banner: true, footer: true }, + ]); + + for (const member of ["banner", "footer"]) { + expect({ member, doc: jsdocOf(member) }).toEqual({ member, doc: expect.stringContaining("CSS") }); + } + }); +}); From 08c16a67c702315f8956a6fd2813229e3ab52398 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 13 Sep 2026 03:36:35 +0000 Subject: [PATCH 4/7] test: cover an HTML output and require the JSDoc to name what decides each result The banner and footer test now builds an HTML entry point, so it sees a JavaScript entry point, a JavaScript chunk, a CSS file and an HTML file. The JSDoc has to name all three output types. The sourcemap: true test also requires the JSDoc to mention outdir, because that is the only difference between the two builds. --- .../integration/bun-types/build-config-jsdoc.test.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/test/integration/bun-types/build-config-jsdoc.test.ts b/test/integration/bun-types/build-config-jsdoc.test.ts index 2ff792b61d24..2eeeb698a8bb 100644 --- a/test/integration/bun-types/build-config-jsdoc.test.ts +++ b/test/integration/bun-types/build-config-jsdoc.test.ts @@ -49,6 +49,8 @@ describe.concurrent("BuildConfig JSDoc matches the runtime", () => { [...sentence.matchAll(/`"(none|linked|inline|external)"`/g)].map(match => match[1]), ); expect(documented).toEqual(expect.arrayContaining(observed)); + // The two builds differ only in `outdir`, so the JSDoc has to say that it decides. + expect(sentences.join(" ")).toContain("`outdir`"); }); test('sourcemap: "linked" and "external" without outdir', async () => { @@ -74,12 +76,13 @@ describe.concurrent("BuildConfig JSDoc matches the runtime", () => { test("banner and footer: JavaScript outputs only", async () => { using dir = tempDir("jsdoc-banner-footer", { + "index.html": ``, "index.js": `import("./lazy.js").then(m => console.log(m.default));`, "lazy.js": `export default "lazy";`, "style.css": `a { color: red; }`, }); const build = await Bun.build({ - entrypoints: [join(String(dir), "index.js"), join(String(dir), "style.css")], + entrypoints: [join(String(dir), "index.html")], splitting: true, banner: "/* BANNER */", footer: "/* FOOTER */", @@ -100,12 +103,17 @@ describe.concurrent("BuildConfig JSDoc matches the runtime", () => { observed.sort((a, b) => (a.ext + a.kind).localeCompare(b.ext + b.kind)); expect(observed).toEqual([ { ext: ".css", kind: "asset", banner: false, footer: false }, + { ext: ".html", kind: "entry-point", banner: false, footer: false }, { ext: ".js", kind: "chunk", banner: true, footer: true }, { ext: ".js", kind: "entry-point", banner: true, footer: true }, ]); + // The build made three types of output file and treated them differently, + // so the JSDoc has to name all three. + const outputTypes = ["CSS", "HTML", "JavaScript"]; for (const member of ["banner", "footer"]) { - expect({ member, doc: jsdocOf(member) }).toEqual({ member, doc: expect.stringContaining("CSS") }); + const doc = jsdocOf(member); + expect({ member, named: outputTypes.filter(type => doc.includes(type)) }).toEqual({ member, named: outputTypes }); } }); }); From d0ddc94fed90236f38dc4b019ceac64ffd8bdaea Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 9 Nov 2026 17:53:52 +0000 Subject: [PATCH 5/7] docs, bun-types: do not say which artifact BuildArtifact.sourcemap is, say that only JavaScript outputs get a map - Remove the statement that a map is the sourcemap property of the artifact it belongs to. That is only true for a build with one output. - "external" makes one map for each JavaScript output file. CSS and HTML output files get no map in any mode. - docs/bundler/index.mdx: leave the sourcemap value table as it is on main and put the note about boolean values above it. - The test now checks each statement on a build that has a JavaScript entry point, a JavaScript chunk, a CSS file and an HTML file. --- docs/bundler/index.mdx | 10 +-- packages/bun-types/bun.d.ts | 15 ++-- .../bun-types/build-config-jsdoc.test.ts | 76 ++++++++++++------- 3 files changed, 62 insertions(+), 39 deletions(-) diff --git a/docs/bundler/index.mdx b/docs/bundler/index.mdx index dde329eb8464..ce8f798f7f91 100644 --- a/docs/bundler/index.mdx +++ b/docs/bundler/index.mdx @@ -740,6 +740,8 @@ Disables environment variable injection entirely. Specifies the type of sourcemap to generate. +`Bun.build` also accepts a boolean. `true` means `"linked"` when `outdir` is set and `"inline"` when it is not. `false` means `"none"`. + ```ts title="build.ts" icon="/icons/typescript.svg" @@ -760,18 +762,12 @@ Specifies the type of sourcemap to generate. | Value | Description | | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `"none"` | Default. No sourcemap is generated. | -| `"linked"` | A separate `*.js.map` file is created alongside each `*.js` bundle using a `//# sourceMappingURL` comment to link the two. You can customize the base URL in this comment with `publicPath` in `Bun.build` or `--public-path` on the CLI.

`js
//

//# sourceMappingURL=bundle.js.map
` | +| `"linked"` | A separate `*.js.map` file is created alongside each `*.js` bundle using a `//# sourceMappingURL` comment to link the two. Requires `--outdir` to be set. You can customize the base URL in this comment with `--public-path`.

`js
//

//# sourceMappingURL=bundle.js.map
` | | `"external"` | A separate `*.js.map` file is created alongside each `*.js` bundle without inserting a `//# sourceMappingURL` comment.

Generated bundles contain a debug id that can be used to associate a bundle with its corresponding sourcemap. This `debugId` is added as a comment at the bottom of the file.

`js
//

//# debugId=
` | | `"inline"` | A sourcemap is generated and appended to the end of the generated bundle as a base64 payload.

`js
//

//# sourceMappingURL=data:application/json;base64,
` | The associated `*.js.map` sourcemap is a JSON file containing an equivalent `debugId` property. -In `Bun.build`, `sourcemap: true` means `"linked"` when `outdir` is set and `"inline"` when it is not. `sourcemap: false` means `"none"`. On the CLI, `--sourcemap` with no value means `linked`. - -On the CLI, a `linked` or `external` sourcemap is a separate file. Set `--outdir` so that Bun has a directory to write it to. - -When `Bun.build` runs without `outdir`, it does not write `"linked"` and `"external"` sourcemaps to disk. Each one is a `BuildArtifact` in `outputs` with `kind: "sourcemap"`, and it is also the `sourcemap` property of the artifact it belongs to. - ### minify Whether to enable minification. Default `false`. diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 7df318295612..2bda19f873c2 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -3406,16 +3406,19 @@ declare module "bun" { * `*.ext` file. A `//# sourceMappingURL` comment is added to the output * file to link the two. * - `"inline"` - an inline source map is appended to the output file. - * - `"external"` - Generate a separate source map file for each output file. - * No `//# sourceMappingURL` comment is added to the output file. + * - `"external"` - Generate a separate source map file for each JavaScript + * output file. No `//# sourceMappingURL` comment is added to the output + * file. + * + * Only JavaScript output files (entry points and chunks) get a source + * map. CSS and HTML output files do not. * * `true` is an alias for `"linked"` when `outdir` is set, and for `"inline"` * when it is not. `false` is an alias for `"none"`. * - * Without `outdir`, `"linked"` and `"external"` maps are not written to - * disk. Each map is a separate {@link BuildArtifact} in `outputs` with - * `kind: "sourcemap"`, and is also the `sourcemap` property of the - * artifact it belongs to. + * Each `"linked"` or `"external"` map is a separate {@link BuildArtifact} + * in `outputs` with `kind: "sourcemap"`. Without `outdir`, Bun does not + * write it to disk. * * @default "none" * diff --git a/test/integration/bun-types/build-config-jsdoc.test.ts b/test/integration/bun-types/build-config-jsdoc.test.ts index 2eeeb698a8bb..7ea7186218a4 100644 --- a/test/integration/bun-types/build-config-jsdoc.test.ts +++ b/test/integration/bun-types/build-config-jsdoc.test.ts @@ -3,7 +3,7 @@ // option describes what the build did. import { describe, expect, test } from "bun:test"; import { tempDir } from "harness"; -import { readFileSync } from "node:fs"; +import { readdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; const dts = readFileSync(join(import.meta.dir, "../../../packages/bun-types/bun.d.ts"), "utf8"); @@ -29,17 +29,30 @@ function sourcemapCommentKind(text: string): "inline" | "linked" | "none" { return "none"; } +const extensionOf = (path: string) => path.slice(path.lastIndexOf(".")); + +// With `splitting`, this page builds to one output of each type: a JavaScript +// entry point, a JavaScript chunk, a CSS file and an HTML file. +const page = { + "index.html": ``, + "index.js": `import("./lazy.js").then(m => console.log(m.default));`, + "lazy.js": `export default "lazy";`, + "style.css": `a { color: red; }`, +}; +const outputTypes = ["CSS", "HTML", "JavaScript"]; + describe.concurrent("BuildConfig JSDoc matches the runtime", () => { test("sourcemap: what `true` is an alias for", async () => { - using dir = tempDir("jsdoc-sourcemap-true", { "index.js": `console.log("hello");` }); - const entrypoints = [join(String(dir), "index.js")]; + using dir = tempDir("jsdoc-sourcemap-true", page); + const config = { entrypoints: [join(String(dir), "index.html")], splitting: true, sourcemap: true }; - const withOutdir = await Bun.build({ entrypoints, sourcemap: true, outdir: join(String(dir), "out") }); - const withoutOutdir = await Bun.build({ entrypoints, sourcemap: true }); - const observed = [ - sourcemapCommentKind(await withOutdir.outputs.find(o => o.kind === "entry-point")!.text()), - sourcemapCommentKind(await withoutOutdir.outputs.find(o => o.kind === "entry-point")!.text()), - ]; + const observed: string[] = []; + for (const build of [await Bun.build({ ...config, outdir: join(String(dir), "out") }), await Bun.build(config)]) { + const scripts = build.outputs.filter(output => extensionOf(output.path) === ".js"); + expect(scripts.map(output => output.kind).sort()).toEqual(["chunk", "entry-point"]); + const kinds = await Promise.all(scripts.map(async output => sourcemapCommentKind(await output.text()))); + observed.push(...new Set(kinds)); + } expect(observed).toEqual(["linked", "inline"]); const sentences = jsdocOf("sourcemap") @@ -54,33 +67,45 @@ describe.concurrent("BuildConfig JSDoc matches the runtime", () => { }); test('sourcemap: "linked" and "external" without outdir', async () => { - using dir = tempDir("jsdoc-sourcemap-no-outdir", { "index.js": `console.log("hello");` }); - const entrypoints = [join(String(dir), "index.js")]; + using dir = tempDir("jsdoc-sourcemap-no-outdir", page); + const sources = readdirSync(String(dir)).sort(); for (const sourcemap of ["linked", "external"] as const) { - const build = await Bun.build({ entrypoints, sourcemap }); - const entry = build.outputs.find(o => o.kind === "entry-point")!; - const map = build.outputs.find(o => o.kind === "sourcemap")!; - expect({ sourcemap, success: build.success, kinds: build.outputs.map(o => o.kind) }).toEqual({ + const build = await Bun.build({ entrypoints: [join(String(dir), "index.html")], splitting: true, sourcemap }); + expect(build.success).toBe(true); + + const maps = build.outputs.filter(output => output.kind === "sourcemap").map(output => output.path); + const observed = build.outputs + .filter(output => output.kind !== "sourcemap") + .map(output => ({ + ext: extensionOf(output.path), + kind: output.kind, + map: maps.includes(output.path + ".map"), + })) + .sort((a, b) => (a.ext + a.kind).localeCompare(b.ext + b.kind)); + expect({ sourcemap, maps: maps.length, observed }).toEqual({ sourcemap, - success: true, - kinds: ["entry-point", "sourcemap"], + maps: 2, + observed: [ + { ext: ".css", kind: "asset", map: false }, + { ext: ".html", kind: "entry-point", map: false }, + { ext: ".js", kind: "chunk", map: true }, + { ext: ".js", kind: "entry-point", map: true }, + ], }); - expect(entry.sourcemap).toBe(map); } + expect(readdirSync(String(dir)).sort()).toEqual(sources); const doc = jsdocOf("sourcemap"); expect(doc).not.toMatch(/requires? `outdir`|outdir\}? (is )?required/i); expect(doc).toContain('`kind: "sourcemap"`'); + // Only one of the three types of output file got a map, so the JSDoc has + // to name all three. + expect(outputTypes.filter(type => doc.includes(type))).toEqual(outputTypes); }); test("banner and footer: JavaScript outputs only", async () => { - using dir = tempDir("jsdoc-banner-footer", { - "index.html": ``, - "index.js": `import("./lazy.js").then(m => console.log(m.default));`, - "lazy.js": `export default "lazy";`, - "style.css": `a { color: red; }`, - }); + using dir = tempDir("jsdoc-banner-footer", page); const build = await Bun.build({ entrypoints: [join(String(dir), "index.html")], splitting: true, @@ -93,7 +118,7 @@ describe.concurrent("BuildConfig JSDoc matches the runtime", () => { build.outputs.map(async output => { const text = await output.text(); return { - ext: output.path.slice(output.path.lastIndexOf(".")), + ext: extensionOf(output.path), kind: output.kind, banner: text.includes("/* BANNER */"), footer: text.includes("/* FOOTER */"), @@ -110,7 +135,6 @@ describe.concurrent("BuildConfig JSDoc matches the runtime", () => { // The build made three types of output file and treated them differently, // so the JSDoc has to name all three. - const outputTypes = ["CSS", "HTML", "JavaScript"]; for (const member of ["banner", "footer"]) { const doc = jsdocOf(member); expect({ member, named: outputTypes.filter(type => doc.includes(type)) }).toEqual({ member, named: outputTypes }); From 86ac7864921e85a0a64836d5d71cebe43893193b Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 9 Nov 2026 19:38:10 +0000 Subject: [PATCH 6/7] test: move the JSDoc checks to test/bundler so that every lane runs them, scope the sourcemap rules to builds without compile - The test runner excludes test/integration/bun-types, so the file never ran in CI. It is now test/bundler/bun-build-jsdoc.test.ts. - The test also checks the sourceMappingURL comment of every output for "linked" and "external". - JSDoc: the rules for true and for builds without outdir are for a build without compile. An executable gets a map file for every value except "none", with or without outdir. - Say "JavaScript entry point and chunk" instead of "JavaScript output": a standalone HTML build inlines the script, with its banner, into the HTML file. - docs/bundler/index.mdx: say that Bun.build does not need outdir for "linked" or "external". --- docs/bundler/index.mdx | 12 ++++---- packages/bun-types/bun.d.ts | 29 ++++++++++-------- .../bun-build-jsdoc.test.ts} | 30 +++++++++++-------- 3 files changed, 40 insertions(+), 31 deletions(-) rename test/{integration/bun-types/build-config-jsdoc.test.ts => bundler/bun-build-jsdoc.test.ts} (86%) diff --git a/docs/bundler/index.mdx b/docs/bundler/index.mdx index ce8f798f7f91..253755b2b35f 100644 --- a/docs/bundler/index.mdx +++ b/docs/bundler/index.mdx @@ -740,7 +740,9 @@ Disables environment variable injection entirely. Specifies the type of sourcemap to generate. -`Bun.build` also accepts a boolean. `true` means `"linked"` when `outdir` is set and `"inline"` when it is not. `false` means `"none"`. +`Bun.build` also accepts a boolean. `false` means `"none"`. `true` means `"linked"` when `outdir` is set and `"inline"` when it is not. + +`Bun.build` does not need `outdir` for `"linked"` or `"external"`. It returns each sourcemap in [`outputs`](#outputs), and without `outdir` it writes nothing to disk. A build with `compile` is different: see [standalone executables](/compiler/executables). @@ -1228,7 +1230,7 @@ A map of file extensions to built-in loader names. Use this to customize how cer A banner added to the final bundle. This can be a directive like `"use client"` for React, or a comment block such as a license. -Bun adds the banner to every JavaScript output file (entry points and chunks). CSS and HTML output files do not get it. +Bun adds the banner to every JavaScript entry point and chunk. It does not add it to CSS or HTML. @@ -1251,7 +1253,7 @@ Bun adds the banner to every JavaScript output file (entry points and chunks). C A footer added to the final bundle. This can be a comment block for a license or a fun easter egg. -Bun adds the footer to every JavaScript output file (entry points and chunks). CSS and HTML output files do not get it. +Bun adds the footer to every JavaScript entry point and chunk. It does not add it to CSS or HTML. @@ -1971,7 +1973,7 @@ interface BuildConfig { /** * Add a banner to the bundled code such as "use client"; * - * Added to every JavaScript output (entry points and chunks), not to CSS or HTML outputs. + * Added to every JavaScript entry point and chunk, not to CSS or HTML. */ banner?: string; /** @@ -1979,7 +1981,7 @@ interface BuildConfig { * * `// made with bun!` * - * Added to every JavaScript output (entry points and chunks), not to CSS or HTML outputs. + * Added to every JavaScript entry point and chunk, not to CSS or HTML. */ footer?: string; diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 2bda19f873c2..3a0c8053ae66 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -3407,22 +3407,25 @@ declare module "bun" { * file to link the two. * - `"inline"` - an inline source map is appended to the output file. * - `"external"` - Generate a separate source map file for each JavaScript - * output file. No `//# sourceMappingURL` comment is added to the output - * file. + * entry point and chunk. No `//# sourceMappingURL` comment is added to + * the output file. * - * Only JavaScript output files (entry points and chunks) get a source - * map. CSS and HTML output files do not. - * - * `true` is an alias for `"linked"` when `outdir` is set, and for `"inline"` - * when it is not. `false` is an alias for `"none"`. + * Only JavaScript entry points and chunks get a source map. CSS and HTML + * do not. * * Each `"linked"` or `"external"` map is a separate {@link BuildArtifact} - * in `outputs` with `kind: "sourcemap"`. Without `outdir`, Bun does not - * write it to disk. + * in `outputs` with `kind: "sourcemap"`. + * + * In a build without `compile`: + * - `true` is an alias for `"linked"` when `outdir` is set, and for + * `"inline"` when it is not. `false` is an alias for `"none"`. + * - Without `outdir`, Bun does not write a `"linked"` or `"external"` map + * to disk. * * @default "none" * * @see {@link publicPath} to customize the base url of linked source maps + * @see [Standalone executables](https://bun.com/docs/compiler/executables) for source maps with `compile` */ sourcemap?: "none" | "linked" | "inline" | "external" | boolean; @@ -3558,8 +3561,8 @@ declare module "bun" { /** * Add a banner to the bundled code such as "use client"; * - * The banner is added to every JavaScript output (entry points and - * chunks). CSS and HTML outputs do not get it. + * The banner is added to every JavaScript entry point and chunk. It is + * not added to CSS or HTML. */ banner?: string; @@ -3568,8 +3571,8 @@ declare module "bun" { * * `// made with bun!` * - * The footer is added to every JavaScript output (entry points and - * chunks). CSS and HTML outputs do not get it. + * The footer is added to every JavaScript entry point and chunk. It is + * not added to CSS or HTML. */ footer?: string; diff --git a/test/integration/bun-types/build-config-jsdoc.test.ts b/test/bundler/bun-build-jsdoc.test.ts similarity index 86% rename from test/integration/bun-types/build-config-jsdoc.test.ts rename to test/bundler/bun-build-jsdoc.test.ts index 7ea7186218a4..137cf8c3f512 100644 --- a/test/integration/bun-types/build-config-jsdoc.test.ts +++ b/test/bundler/bun-build-jsdoc.test.ts @@ -6,7 +6,7 @@ import { tempDir } from "harness"; import { readdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; -const dts = readFileSync(join(import.meta.dir, "../../../packages/bun-types/bun.d.ts"), "utf8"); +const dts = readFileSync(join(import.meta.dir, "../../packages/bun-types/bun.d.ts"), "utf8"); /** The JSDoc block directly above `member?:` in bun.d.ts, as one line of prose. */ function jsdocOf(member: string): string { @@ -75,22 +75,26 @@ describe.concurrent("BuildConfig JSDoc matches the runtime", () => { expect(build.success).toBe(true); const maps = build.outputs.filter(output => output.kind === "sourcemap").map(output => output.path); - const observed = build.outputs - .filter(output => output.kind !== "sourcemap") - .map(output => ({ - ext: extensionOf(output.path), - kind: output.kind, - map: maps.includes(output.path + ".map"), - })) - .sort((a, b) => (a.ext + a.kind).localeCompare(b.ext + b.kind)); + const observed = await Promise.all( + build.outputs + .filter(output => output.kind !== "sourcemap") + .map(async output => ({ + ext: extensionOf(output.path), + kind: output.kind, + map: maps.includes(output.path + ".map"), + comment: sourcemapCommentKind(await output.text()), + })), + ); + observed.sort((a, b) => (a.ext + a.kind).localeCompare(b.ext + b.kind)); + const comment = sourcemap === "linked" ? "linked" : "none"; expect({ sourcemap, maps: maps.length, observed }).toEqual({ sourcemap, maps: 2, observed: [ - { ext: ".css", kind: "asset", map: false }, - { ext: ".html", kind: "entry-point", map: false }, - { ext: ".js", kind: "chunk", map: true }, - { ext: ".js", kind: "entry-point", map: true }, + { ext: ".css", kind: "asset", map: false, comment: "none" }, + { ext: ".html", kind: "entry-point", map: false, comment: "none" }, + { ext: ".js", kind: "chunk", map: true, comment }, + { ext: ".js", kind: "entry-point", map: true, comment }, ], }); } From 4a4e7e8ecd8da1ae519e261bb17ab96871ab3532 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 10 Nov 2026 00:14:28 +0000 Subject: [PATCH 7/7] test: check that a build without outdir leaves no file at the path of a sourcemap artifact Without outdir the artifact paths are relative to the cwd, so a write from an in-memory build would land there and not in the source directory that the test already lists. --- test/bundler/bun-build-jsdoc.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/bundler/bun-build-jsdoc.test.ts b/test/bundler/bun-build-jsdoc.test.ts index 137cf8c3f512..101448d76f8f 100644 --- a/test/bundler/bun-build-jsdoc.test.ts +++ b/test/bundler/bun-build-jsdoc.test.ts @@ -3,7 +3,7 @@ // option describes what the build did. import { describe, expect, test } from "bun:test"; import { tempDir } from "harness"; -import { readdirSync, readFileSync } from "node:fs"; +import { existsSync, readdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; const dts = readFileSync(join(import.meta.dir, "../../packages/bun-types/bun.d.ts"), "utf8"); @@ -75,6 +75,9 @@ describe.concurrent("BuildConfig JSDoc matches the runtime", () => { expect(build.success).toBe(true); const maps = build.outputs.filter(output => output.kind === "sourcemap").map(output => output.path); + // Without `outdir` the paths are relative to the cwd and hold a content + // hash, so a file at one of them can only come from this build. + expect(maps.filter(path => existsSync(path))).toEqual([]); const observed = await Promise.all( build.outputs .filter(output => output.kind !== "sourcemap")