From cd8043b76eabc9f0daa9dffe92cb8e4c0a80a770 Mon Sep 17 00:00:00 2001 From: robobun Date: Tue, 21 Oct 2025 14:25:08 -0700 Subject: [PATCH] Fix Bun.build() compile API to properly apply sourcemaps (#23916) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes a bug where the `Bun.build()` API with `compile: true` did not properly apply sourcemaps, even when `sourcemap: "inline"` was specified. This resulted in error stack traces showing bundled virtual paths (`/$bunfs/root/`) instead of actual source file names and line numbers. ## Problem The CLI `bun build --compile --sourcemap` worked correctly, but the equivalent API call did not: ```javascript // This did NOT work (before fix) await Bun.build({ entrypoints: ['./app.js'], compile: true, sourcemap: "inline" // <-- Was ignored/broken }); ``` Error output showed bundled paths: ``` error: Error from helper module at helperFunction (/$bunfs/root/app.js:4:9) // ❌ Wrong path at main (/$bunfs/root/app.js:9:17) // ❌ Wrong line numbers ``` ## Root Cause The CLI explicitly overrides any sourcemap type to `.external` when compile mode is enabled (in `/workspace/bun/src/cli/Arguments.zig`): ```zig // when using --compile, only `external` works if (ctx.bundler_options.compile) { opts.source_map = .external; } ``` The API implementation in `JSBundler.zig` was missing this override. ## Solution Added the same sourcemap override logic to `JSBundler.zig` when compile mode is enabled: ```zig // When using --compile, only `external` sourcemaps work, as we do not // look at the source map comment. Override any other sourcemap type. if (this.source_map != .none) { this.source_map = .external; } ``` Now error output correctly shows source file names: ``` error: Error from helper module at helperFunction (helper.js:2:9) // ✅ Correct file at main (app.js:4:3) // ✅ Correct line numbers ``` ## Tests Added comprehensive test coverage in `/workspace/bun/test/bundler/bun-build-compile-sourcemap.test.ts`: - ✅ `sourcemap: "inline"` works - ✅ `sourcemap: true` works - ✅ `sourcemap: "external"` works - ✅ Multiple source files show correct file names - ✅ Without sourcemap, bundled paths are shown (expected behavior) All tests: - ✅ Fail with `USE_SYSTEM_BUN=1` (confirms bug exists) - ✅ Pass with `bun bd test` (confirms fix works) - ✅ Use `tempDir()` to avoid disk space issues 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Claude Bot Co-authored-by: Claude --- src/bun.js/api/JSBundler.zig | 6 + .../bun-build-compile-sourcemap.test.ts | 147 ++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 test/bundler/bun-build-compile-sourcemap.test.ts diff --git a/src/bun.js/api/JSBundler.zig b/src/bun.js/api/JSBundler.zig index 2dfe0a5727..7c8673868c 100644 --- a/src/bun.js/api/JSBundler.zig +++ b/src/bun.js/api/JSBundler.zig @@ -694,6 +694,12 @@ pub const JSBundler = struct { const base_public_path = bun.StandaloneModuleGraph.targetBasePublicPath(this.compile.?.compile_target.os, "root/"); try this.public_path.append(base_public_path); + // When using --compile, only `external` sourcemaps work, as we do not + // look at the source map comment. Override any other sourcemap type. + if (this.source_map != .none) { + this.source_map = .external; + } + if (compile.outfile.isEmpty()) { const entry_point = this.entry_points.keys()[0]; var outfile = std.fs.path.basename(entry_point); diff --git a/test/bundler/bun-build-compile-sourcemap.test.ts b/test/bundler/bun-build-compile-sourcemap.test.ts new file mode 100644 index 0000000000..b26c63ebca --- /dev/null +++ b/test/bundler/bun-build-compile-sourcemap.test.ts @@ -0,0 +1,147 @@ +import { describe, expect, test } from "bun:test"; +import { bunEnv, tempDir } from "harness"; +import { join } from "path"; + +describe("Bun.build compile with sourcemap", () => { + const helperFiles = { + "helper.js": `export function helperFunction() { + throw new Error("Error from helper module"); +}`, + "app.js": `import { helperFunction } from "./helper.js"; + +function main() { + helperFunction(); +} + +main();`, + }; + + async function testSourcemapOption(sourcemapValue: "inline" | "external" | true, testName: string) { + using dir = tempDir(`build-compile-sourcemap-${testName}`, helperFiles); + + const result = await Bun.build({ + entrypoints: [join(String(dir), "app.js")], + compile: true, + sourcemap: sourcemapValue, + }); + + expect(result.success).toBe(true); + expect(result.outputs.length).toBe(1); + + const executablePath = result.outputs[0].path; + expect(await Bun.file(executablePath).exists()).toBe(true); + + // Run the compiled executable and capture the error + await using proc = Bun.spawn({ + cmd: [executablePath], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + + const [_stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + // With sourcemaps working, we should see the actual file names + expect(stderr).toContain("helper.js"); + expect(stderr).toContain("app.js"); + + // Should NOT see the bundled virtual path (/$bunfs/root/ on Unix, B:/~BUN/root/ on Windows) + expect(stderr).not.toMatch(/(\$bunfs|~BUN)\/root\//); + + // Verify it failed (the error was thrown) + expect(exitCode).not.toBe(0); + } + + test.each([ + ["inline" as const, "inline"], + [true as const, "true"], + ["external" as const, "external"], + ])("compile with sourcemap: %s should work", async (sourcemapValue, testName) => { + await testSourcemapOption(sourcemapValue, testName); + }); + + test("compile without sourcemap should show bundled paths", async () => { + using dir = tempDir("build-compile-no-sourcemap", helperFiles); + + const result = await Bun.build({ + entrypoints: [join(String(dir), "app.js")], + compile: true, + // No sourcemap option + }); + + expect(result.success).toBe(true); + expect(result.outputs.length).toBe(1); + + const executablePath = result.outputs[0].path; + expect(await Bun.file(executablePath).exists()).toBe(true); + + // Run the compiled executable and capture the error + await using proc = Bun.spawn({ + cmd: [executablePath], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + + const [_stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + // Without sourcemaps, we should see the bundled virtual path (/$bunfs/root/ on Unix, B:/~BUN/root/ on Windows) + expect(stderr).toMatch(/(\$bunfs|~BUN)\/root\//); + + // Verify it failed (the error was thrown) + expect(exitCode).not.toBe(0); + }); + + test("compile with multiple source files", async () => { + using dir = tempDir("build-compile-sourcemap-multiple-files", { + "utils.js": `export function utilError() { + throw new Error("Error from utils"); +}`, + "helper.js": `import { utilError } from "./utils.js"; +export function helperFunction() { + utilError(); +}`, + "app.js": `import { helperFunction } from "./helper.js"; + +function main() { + helperFunction(); +} + +main();`, + }); + + const result = await Bun.build({ + entrypoints: [join(String(dir), "app.js")], + compile: true, + sourcemap: "inline", + }); + + expect(result.success).toBe(true); + const executable = result.outputs[0].path; + expect(await Bun.file(executable).exists()).toBe(true); + + // Run the executable + await using proc = Bun.spawn({ + cmd: [executable], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + + const [_stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + // With sourcemaps, should show all three source file names + expect(stderr).toContain("utils.js"); + expect(stderr).toContain("helper.js"); + expect(stderr).toContain("app.js"); + + // Should NOT show bundled paths (/$bunfs/root/ on Unix, B:/~BUN/root/ on Windows) + expect(stderr).not.toMatch(/(\$bunfs|~BUN)\/root\//); + + // Verify it failed (the error was thrown) + expect(exitCode).not.toBe(0); + }); +});