Skip to content

bun build --compile: only write .map sidecar for --sourcemap=external - #36384

Closed
robobun wants to merge 4 commits into
mainfrom
claude/farm-9b041bc4-compile-sourcemap-sidecar
Closed

robobun wants to merge 4 commits into
mainfrom
claude/farm-9b041bc4-compile-sourcemap-sidecar

Conversation

@robobun

@robobun robobun commented Jul 29, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #8341

Repro

$ printf 'function f(){throw new Error("x")}\nf()\n' > thr.ts
$ bun build --compile thr.ts --outfile app --sourcemap
$ ls
app  thr.js.map  thr.ts           # <- thr.js.map is dead weight
$ rm thr.js.map && ./app
error: x
      at f (thr.ts:1:20)          # <- stack still resolves; map is embedded

--sourcemap, =inline, =linked, and =external all produced a byte-identical executable and all wrote the sidecar. The sidecar is named after the entry (<entry>.js.map), so it can clobber a project's own map file sitting next to the outfile. Bun.build({ compile: true, sourcemap: 'inline' | 'linked' | true }) had the same behavior.

Cause

Both the CLI (build_command.rs) and the JS API (JSBundler.rs) force source_map = External for the executable compile path so the standalone module graph receives the map as a separate OutputFile to embed (the runtime does not read //# sourceMappingURL). The post-compile sidecar write then checked the post-override value, so it was true whenever any sourcemap mode was set.

Fix

Record whether the user's original choice was external before the override runs, and gate the sidecar write on that in both paths. --sourcemap / =inline / =linked (and sourcemap: true | 'inline' | 'linked') now embed only; =external / 'external' still writes the .map next to the executable.

Verification

New parameterized tests in test/bundler/bun-build-compile-sourcemap.test.ts cover both entry points: a CLI block over all four flag spellings and a JS API block over true/'linked'/'inline'/'external'. Each asserts no .map in the output dir for non-external modes, exactly one for external, and that the embedded map still resolves thr.ts in the thrown stack. 6/8 new cases fail on main, 17/17 pass with the fix.


no test proof · iteration 0 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/bundler/bun-build-compile-sourcemap.test.ts

--compile internally forces the bundler's source_map option to External
so the standalone module graph receives the map as a separate OutputFile
to embed. The post-compile sidecar write checked that post-override
value, so any non-none --sourcemap mode (bare/linked/inline) also
dropped an <entry>.js.map next to the outfile even though the map is
already embedded in the executable.

Record whether the user explicitly requested external before the
override and gate the sidecar write on that instead.

Closes #8341
@robobun

robobun commented Jul 29, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: diff is green. 17/17 pass in bun-build-compile-sourcemap.test.ts locally and in CI.

The CI failure on this PR is test/js/web/fetch/body-mixin-errors.test.ts timing out, which is also red on main at 8afcd4b (introduced by #35855) and unrelated to this bundler change. Everything else is pre-existing flake.

Reproduced with:

printf 'function f(){throw new Error("x")}\nf()\n' > thr.ts
bun build --compile thr.ts --outfile app --sourcemap
ls thr.js.map   # exists before this PR, gone after

Also covers the Bun.build({ compile: true, sourcemap: 'inline' }) path (same bug, caught in review).

@coderabbitai

coderabbitai Bot commented Jul 29, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 15 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6310822b-e0ca-4e52-a29c-47952e4c0a41

📥 Commits

Reviewing files that changed from the base of the PR and between 8afcd4b and 64ffca1.

📒 Files selected for processing (4)
  • src/runtime/api/JSBundler.rs
  • src/runtime/api/js_bundle_completion_task.rs
  • src/runtime/cli/build_command.rs
  • test/bundler/bun-build-compile-sourcemap.test.ts

Comment @coderabbitai help to get the list of available commands.

Comment thread src/runtime/cli/build_command.rs Outdated
Comment thread src/runtime/cli/build_command.rs Outdated
Same fix as the CLI path. JSBundler forces source_map to External so the
standalone graph receives the map as a separate OutputFile; the
completion task then wrote every Sourcemap output to disk regardless of
whether the user requested an external file. Record the user's choice on
CompileOptions before the override and gate the sidecar write on it.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — the JS API path is now fixed alongside the CLI path, addressing my earlier review.

What was reviewed:

  • compile_sourcemap_sidecar / CompileOptions.sourcemap_sidecar capture the pre-override choice before both entry points force External; gates in build_command.rs:937 and js_bundle_completion_task.rs:447 are correctly placed.
  • Confirmed the standalone-HTML branch is unaffected (that path clears config.compile so do_compilation never runs), and that sourcemap: true / string-target / object compile forms all reach the override block that sets the flag.
  • Existing external + splitting tests still hit sourcemap_sidecar = true; the removed opt_source_map snapshot had no other consumers.
Extended reasoning...

Overview

Fixes #8341: bun build --compile --sourcemap[=inline|linked] and Bun.build({ compile: true, sourcemap: 'inline'|'linked'|true }) were writing a stray <entry>.js.map sidecar even though the map is embedded in the executable. The fix records whether the user's original choice was external before the compile path forces source_map = External (which it must, since the standalone module graph consumes the map as a separate OutputFile), and gates the post-compile disk write on that recorded intent. Applied symmetrically to both the CLI (build_command.rs) and the JS API (JSBundler.rs → js_bundle_completion_task.rs), with parameterized tests covering all four sourcemap spellings on each entry point.

Prior review

My previous review on this PR flagged that the JS API path had the identical bug and wasn't fixed. Commit 64ffca1 addressed it exactly as suggested: added CompileOptions.sourcemap_sidecar: bool (default false), set it in Config::from_js before the override, and added && compile_options.sourcemap_sidecar to the sidecar-write branch in do_compilation. A matching describe.each test block was added asserting both no .map on disk and no kind: "sourcemap" in result.outputs for non-external modes. That resolves the finding.

Security risks

None. This only narrows when a .map file is written next to a compiled executable; no new inputs are parsed, no paths are derived from untrusted data beyond what already existed.

Level of scrutiny

Low-to-medium. The change is a small boolean gate on existing write paths; the tricky part (why the override-to-External exists at all) is untouched. I traced the flag through every construction site of CompileOptions (default, boolean compile: true, string compile: 'bun-…', object compile: {…}, and target: 'bun-…') — all reach the if let Some(compile) = this.compile.as_mut() block that sets sourcemap_sidecar from the pre-override this.source_map, so no path leaves it stale. The standalone-HTML branch skips that block, but it also nulls config.compile in configure_bundler, so do_compilation (the only reader) never runs for it.

Other factors

  • The removed opt_source_map snapshot in build_command.rs had exactly one consumer (the line being replaced); grep confirms no other references.
  • Existing tests in the same file (external writes .map, splitting writes multiple .map, subdir outfile) all use 'external' and remain green under the new gate. The pre-existing testSourcemapOption and "multiple source files" tests use 'inline'/true and only assert the embedded map resolves — they don't index into result.outputs in a way the now-dropped sourcemap entry would break.
  • Test hygiene looks good: tempDir + using, per-mode temp dirs, subprocess pipes drained concurrently, exit code asserted last, CLI cases use test.concurrent.

@robobun

robobun commented Jul 29, 2026 •

Copy link
Copy Markdown
Collaborator Author

Jarred-Sumner pushed a commit that referenced this pull request Sep 3, 2026
…nks can load it (#41264)

### Problem
- In an executable built with `--compile --splitting`, an `import()` or
`require()` of the entry point from another chunk fails:
`ResolveMessage: Cannot find module '/$bunfs/root/main.js' imported from
/$bunfs/root/tool.js`. It happens with `bun build` and `Bun.build`, on
1.4.1-canary.1 and main.
- The linker prints the path of the entry point's chunk, `main.js`, into
the other chunks. After linking, the CLI and `Bun.build` renamed that
output file to the name of the outfile (`build_command.rs:807`,
`js_bundle_completion_task.rs:334`). So the executable has no
`/$bunfs/root/main.js`.

### Fix
- `compute_chunks` names the entry point's chunk after the outfile (new
option `compile_entry_point_name`). It is the chunk that
`StandaloneModuleGraph::to_bytes` embeds as the entry point: the first
server-side `EntryPoint` (`chunk_side`, `chunk_output_kind`). The
post-link renames are removed.
- Each path printed for the chunk now names the embedded module:
`import()`, `import.meta.require`, module records, and the bytecode
source URL.
- Visible change: the external source map of the entry point is
`<outfile>.map`, not `<entry>.js.map`. Its metafile output is
`./<outfile>`.
- Verified: 5 new tests in
`test/bundler/bundler_compile_splitting.test.ts` and 1 stricter test in
`bun-build-compile-sourcemap.test.ts`, which fail on 1.4.1-canary.1.
Self-reviewed: 4 concerns raised, 4 addressed. The notes list the other
suites.

### Background
- `--compile` embeds each output file at `/$bunfs/root/<path>`. The
entry point is embedded under the outfile's name, so `import.meta.path`
is `/$bunfs/root/<outfile>`.
- With `--splitting`, a chunk refers to another chunk by its output
path. In an executable, that path starts with `/$bunfs/root/`.
- Since #40519, a split `require()` of an ES module is a chunk boundary.
It prints `import.meta.require("<path>")`.

<details><summary>Notes</summary>

- Found while testing another change. No issue reports it.
- Released behavior (bun 1.4.0): with one entry point, a lazy chunk's
`import()` of it fails, and `require()` of it works. `require()` fails
on main because of #40519, which is not released. With two entry points,
1.4.0 runs the program, but the main entry point runs twice.
- The fix covers specifiers that the bundler resolves. A computed
specifier, such as `import(name)`, still resolves at run time against
the embedded names, as before.
- Source map name: `bun build --compile --sourcemap` writes the external
map for every mode (the mode is forced to `external`,
`build_command.rs:321`). So a script that reads `<entry>.js.map` next to
the executable must read `<outfile>.map` now. The new name matches the
module name in stack traces. Two executables built from the same entry
name into one directory no longer write the same map file. #36384 (open)
changes which modes write the map file. It does not change the name.
- For an empty outfile, `.`, `..` and `../`, the CLI writes the
executable as `index`. The entry point is now embedded as `index` too
(`compile_outfile`). Before, `--outfile .` embedded it as
`/$bunfs/root/`, and relative specifiers from the entry point did not
resolve. Main has the same problem.
- An outfile name that equals the chunk name of another entry point
(`--outfile tool.js` with `tool.ts` as the second entry point) is now a
"Multiple files share the same output path" error. Before, the build
succeeded, and the executable loaded the main entry point in place of
`tool.js`.
- `chunk_side` and `chunk_output_kind` have the same text and position
as in #41235, so the two PRs merge without a conflict. #41235 also finds
the main module in `link` with the same rule.
- `writeOutputFilesToDisk.rs` uses the two helpers too. Its old `side`
code did not check `IS_BROWSER_CHUNK_FROM_SERVER_BUILD`, so a shared
chunk with a browser file that is not its first file now gets `Client`
there, as in memory. Nothing reads `side` for files written to disk.
- `mergeSmallChunks.rs` still pins the chunks of user entry points under
`--compile`. Its comment named the rename as the reason. The reason is
gone, but the pin stays, because a removal changes the default chunk
layout of split executables. #40601 excludes `--compile` for the same
reason.
- Tests that fail on the debug build with and without this diff: 3
bytecode tests in `bun-build-compile.test.ts` and 2 in
`bun-build-api.test.ts` (timeouts), and
`compile/HelloWorldWithProcessVersionsBun`.
- Suites run on the debug build: `bundler_compile_splitting`,
`bundler_compile`, `bun-build-compile`, `bun-build-compile-sourcemap`,
`compile-asset-bunfs`, `compile-sourcemap-internal`,
`bundler_compile_autoload`, `compile-argv`, `compile-process-execargv`,
`bundler_html_server`, `standalone`, `html-import-manifest`, `metafile`,
`bundler_splitting`, `bun-build-api`, `bundler_edgecase`.
- Checked by hand: `--bytecode --format=esm` (the entry point and the
other chunk load from bytecode), `--minify`, `--sourcemap=external`,
entry points in different directories, a static and a dynamic import of
the entry point in one file, and a server entry point with an HTML
import.

</details>

<!-- robobun:evidence:begin -->

---

**no test proof** · iteration 1 · platform-specific test(s) that do not
run on this machine, deferring to CI, which covers all platforms:
test/bundler/bundler_compile_splitting.test.ts,
test/bundler/bun-build-compile-sourcemap.test.ts

<!-- robobun:evidence:end -->
@robobun

robobun commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator Author

Closing as part of a cleanup of stale pull requests. This PR has had no new commits since 2026-07-29, it conflicts with main, and its last CI run failed. This is not a judgment on the fix itself. If the problem still reproduces on a current build, reopen this PR after a rebase or open a new one against main.

@robobun robobun closed this Sep 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants