Skip to content

bake: turn split require off for production builds - #46112

Open
robobun wants to merge 4 commits into
mainfrom
robobun/e49fade9/bake-production-split-require
Open

robobun wants to merge 4 commits into
mainfrom
robobun/e49fade9/bake-production-split-require

Conversation

@robobun

@robobun robobun commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • bun build --app exits 1 when server code calls require() on an ES module: error: Cannot find module './sf6jz4sb.js' from '/bake:/_bun/qj34pke0.js'. In try/catch it exits 0 with the wrong page. bun 1.4.0 builds both (regression from bundler: with --splitting --target bun, require() of an ES module is a chunk boundary #40519).
  • Split require prints the call as import.meta.require("./<chunk>.js") (src/bundler/bundle_v2.rs:2264), which looks for a file. The build keeps server chunks in memory. Framework::init_transpiler (src/runtime/bake/config/transpiler.rs:49) never set split_require.

Fix

  • init_transpiler sets split_require = false. The required module stays in the calling chunk behind its lazy wrapper, as in 1.4.0.
  • Correct because the build loads every server chunk from memory. A chunk per require() saves nothing.
  • Verified: test/bake/dev/production.test.ts (new case, fails on main), test/bake/dev/esm.test.ts, the SplitRequire bundler tests.
  • Self-reviewed: 5 concerns raised, 5 addressed.

Background

Downsides

  • A page that reaches a client component only through a server-side require() loads client code again, as in 1.4.0.
  • With client code and a server-side require() of an ES module, the runtime chunk that the browser loads grows from 867 to 1,292 bytes. Without that require(), dist/ is byte-identical.
  • A require() that never runs no longer hides an import that the build cannot load (node:sea, see Notes).
Notes

Where this comes from

Each page of the test, built as its own app (release builds of main at c1df64b, without and with the change)

Page Without the change With the change
A CommonJS file requires the ES module exit 1 app: 1.2.3 function
The page requires it exit 1 in-page: 1.2.3
A function requires it during the render exit 1 lazy: undefined 1.2.3 true 1
A client component requires it (SSR graph) exit 1 client: 1.2.3
The required module renders a client component exit 1 island: button, one module script
require() in try/catch exit 0, optional: missing optional: 1.2.3
require() and import() of one module exit 1 both: 1.2.3 true 1
Controls (JSON, CommonJS, a builtin, a require() that never runs, import()) same text same text

The test passes on bun 1.4.0 too, so every expected value is what 1.4.0 rendered.

Why this place

  • find_reachable_files reads split_require once, from the server transpiler (src/bundler/bundle_v2.rs:2388). bun build and Bun.build assign the field next to code_splitting. Bake turned code splitting on and kept the default true.
  • The store is unconditional. The dev server has code splitting off and never reads the flag. The server and SSR graphs share the one read.
  • The bundler, the printer, the CommonJS builtins and the bake loader hooks are not changed.

Measurements (release and debug builds of main at c1df64b, without and with the change)

  • Release binary text: +0 bytes (size: 106,785,051 both).
  • bun build --splitting --target bun on the bench/split-require app: 0 of 42 output files differ between the two release binaries (sha256). With --no-split-require: 0 of 1.
  • File syscalls that name /bake: during bun build --app on the repro: 4 before (all openat, all ENOENT), 0 after (gdb catch syscall, release builds).
  • Repro, debug builds: module-map entries 5 to 4, in-memory loads 4 to 4. With --debug-dump-server-files --debug-no-minify: server JS files 6 to 5, the calling chunk 774 to 1,106 bytes, relative import.meta.require call sites 1 to 0.
  • A require() that never runs: module-map entries 5 to 4. The same module also imported statically: entries 6 to 4 and loads 5 to 4. A control app: 4 and 4 on both.
  • The first require() of an ES module in a server chunk creates 16 cells (Object 9, Function 3, GetterSetter 3, WeakMap 1) plus its code and strings. Each later call creates 0 (heapStats().objectTypeCounts, debug build).
  • A require() of an ES module that is already loaded, on disk output of one release binary: 20.4 ns per call with the wrapper, 691.4 ns with the split form (median of 10 interleaved runs of 2,000,000 calls, noise floor 4.3 ns).
  • First Downsides bullet, measured with a require() that never runs and reaches a client component: index.html is 545 bytes with no module script before, 681 bytes with one after and on 1.4.0. That script loads 199,064 bytes of JavaScript.
  • Second Downsides bullet: the runtime chunk holds the helpers of both graphs (bake production build tracking issue #14763 lists this, bundler: give the browser side of a server build its own copy of the runtime #38376 changes it). The growth is __toCommonJS and __export, which a wrapped ES module needs.
  • Assignments added: 2 or 3 per bun build --app and per dev server start (one per graph). 1 of the 6 init_transpiler call sites has a reader.

The node:sea case

  • The production build cannot load node:sea from server code: Non-relative import "node:sea" from "bake:/_bun/..." are not allowed in production assets. That is the same on 1.4.0, on main and on this branch.
  • A module that imports it, reached only by a require() that never runs or that is in try/catch: exit 1 on 1.4.0, exit 0 on main, exit 1 on this branch.
  • The split form left the imports of the required module to the call. The wrapper form links them when the calling chunk loads.

Not in this PR (same result on 1.4.0, on main and on this branch)

  • require("./addon.node") in server code: Cannot find module './hvncczhx.node' from '/bake:/_bun/...'.
  • import db from "./data.sqlite" with { type: "sqlite", embed: "true" }: Cannot find module './arerxbxt.sqlite' from '/bake:/_bun/...'.
  • A hand-written import.meta.require("../lib/dep.mjs"): Cannot find module '../lib/dep.mjs' from '/bake:/_bun/...'.
  • require() of a "use client" module: TypeError: Cannot destructure property 'default' from null or undefined value.

These do not read split_require. The first three end in an import.meta.require() of a path that does not exist next to the in-memory chunk. No issue tracks them yet.

The test

Other suites on the debug build with the change

  • test/bake/dev/esm.test.ts: 18 pass.
  • test/bundler/bundler_splitting.test.ts -t SplitRequire: 24 pass.
  • test/bundler/bundler_compile_splitting.test.ts -t SplitRequire: 2 pass.
  • test/bundler/bundler_dynamic_import_dce.test.ts: 333 pass.

Designs weighed

  • A new default in the bundler options (split_require off unless a driver sets it): the same output for bake, but it changes the starting value for every other user of those options.
  • A bake-only import.meta.require that loads bake:/ chunks: new loader code to keep a chunk boundary that a build with every chunk in memory does not use. require() would also return the module namespace object, not the copy that 1.4.0 returned.
  • A bake:/ branch in the shared synchronous resolve path: see Background.

no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/bake/dev/production.test.ts

The production build loads server chunks from memory under bake:/ keys.
With split require on, the bundler printed a require() of an ES module
as import.meta.require("./<chunk>.js"), which the file-system resolver
cannot find there. The build failed with:

  error: Cannot find module './<chunk>.js' from '/bake:/_bun/<chunk>.js'

Framework::init_transpiler now sets split_require to false, so the
required module stays in the calling chunk behind its lazy wrapper.
@robobun

robobun commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status

  • Reproduced on bun 1.4.3-canary (367d939) and on debug builds of main (c1df64b and 2d6d445). An app whose server code calls require() on an ES module: bun build --app ./src/index.tsx --outdir ./dist exits 1 with error: Cannot find module './sf6jz4sb.js' from '/bake:/_bun/qj34pke0.js'. bun 1.4.0 builds the same app.
  • The new case in test/bake/dev/production.test.ts holds the repro. It fails on main and passes with the change, on a debug build and on a release build.
  • The fix is in this PR: bake: turn split require off for production builds #46112.

@robobun

robobun commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 9:39 AM PT - Oct 21st, 2026

✅ @robobun, your commit d4e03cd372e82f12adad90cf67bf6d1c56229797 passed in Build #130078! 🎉


🧪   To try this PR locally:

bunx bun-pr 46112

That installs a local version of the PR into your bun-46112 executable, so you can run:

bun-46112 --bun

@coderabbitai

coderabbitai Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: oven-sh/bun/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: e5978277-3542-44c1-accb-99d5cfadb1a6
📥 Commits

Reviewing files that changed from the base of the PR and between 351378a and d4e03cd.

📒 Files selected for processing: 1
  • test/bake/dev/production.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 0 remain after this review.

Walkthrough

Changes

Production require loading

Layer / File(s) Summary
Configure and test require loading
src/runtime/bake/config/transpiler.rs, test/bake/dev/production.test.ts
The transpiler disables split_require for all configurations. The production test covers ES module require() calls across server-rendered and client-component routes, lazy calls, guarded calls, shared module loading, and existing require cases.

Suggested reviewers: alii

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to d4e03

The production test exercises the guarded require path, and no merge-blocking issue is established.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title clearly and concisely describes the primary change: disabling split require for production Bake builds.
Description check Passed The description explains the problem, fix, rationale, tradeoffs, and verification results. It does not use the template headings exactly, but it provides the required content in equivalent sections.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@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.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline findings, I also checked whether the unconditional split_require = false changes the dev server or client graph: find_reachable_files (src/bundler/bundle_v2.rs:2379) only consults the flag under code_splitting, which Bake disables in Development, and the lazy-chunk gate at :2239 already requires a Bun-target importer, so browser-side files never split a require() either way.

Extended reasoning...

The diff adds one unconditional option assignment in Framework::init_transpiler and one serial production-build test; it touches no security-sensitive surface. The flag's only consumer is the reachable-file visitor in bundle_v2.rs, and the dev-server and client-graph paths were confirmed unaffected, leaving the two inline findings (the try/catch-require exit-code regression and the slow serial test) as the open points for a human to weigh.

Comment thread src/runtime/bake/config/transpiler.rs
Comment thread test/bake/dev/production.test.ts Outdated
The case spawns its own build in its own directory, so it does not
depend on its neighbours.
Comment thread src/runtime/bake/config/transpiler.rs Outdated

@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.

Re-reviewed after the second push: no new bugs found, and moving the new case to test.concurrent matches the sibling test.concurrent.each cases in this file. A human look is still worthwhile because the one-line change unconditionally alters how every Bake production build bundles a server-side require() of an ES module, and the author-documented tradeoffs (wrapped module keeps its imports linked eagerly; larger runtime chunk when client code is present) are a product call rather than a correctness question.

What was reviewed:

  • Traced split_require to its single bundler reader (find_reachable_files, src/bundler/bundle_v2.rs:2363), only reached when code_splitting is on — so the dev server (code splitting off) never reads it and the unconditional assignment for Client/Server/Ssr is inert there; the Client graph is also excluded by the is_bun() target check at :2240.
  • Confirmed bun build (src/runtime/cli/build_command.rs:233) and Bun.build (src/runtime/api/js_bundle_completion_task.rs:1086) set this same field per driver, so a driver-level override is the established pattern rather than a bundler default change.
  • Test: bunEnv is spread into the subprocess env, assertions on rendered output precede the exit-code check, and tempDirWithBakeDeps without using matches every sibling in the describe block.
Extended reasoning...

The diff adds one unconditional option assignment in Framework::init_transpiler (src/runtime/bake/config/transpiler.rs:52) and one test.concurrent case in test/bake/dev/production.test.ts covering eight require()-of-ESM shapes in a Bake production build. It touches no security-sensitive surface: no auth, crypto, path handling, or untrusted-input parsing. The code is small and follows the same per-driver pattern the CLI and JS API use for this option, and the second commit addressed the earlier serial-test note. Not approved because the change is a behavioral tradeoff for all Bake production builds that a maintainer should weigh (the PR itself lists downsides such as a never-executed require() no longer hiding an unloadable import), rather than a purely mechanical fix.

Still open from earlier reviews (1):

  • Unresolved: 1 minor or pre-existing.

@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.

I re-reviewed the latest push and found no bugs; the test.concurrent change from the earlier round is in. What remains is a design call rather than a defect: with split_require off, a try/catch or never-executed require() of an ES module whose imports the production loader cannot serve now fails the build where the base exited 0 (the description lists this as a downside, matching 1.4.0), so a maintainer should weigh that trade-off before merging.

What was reviewed:

  • Traced split_require from init_transpiler to its single reader in find_reachable_files (src/bundler/bundle_v2.rs:2363): the server transpiler is the primary transpiler for generate_from_bake_production_cli, so the assignment is live, and nothing later in init_transpiler (sync_resolver_opts, configure_linker) overwrites it.
  • Checked the new test: tempDirWithBakeDeps, normalizePath, and path are existing imports in the file; the toEqual map over all rendered pages and the exact withClientCode list cannot pass vacuously.
  • Checked .github/CODEOWNERS: neither changed file is owned.
Extended reasoning...

The diff adds one unconditional assignment in src/runtime/bake/config/transpiler.rs (split_require = false in Framework::init_transpiler, applied to every Bake graph in both modes) and a test.concurrent regression test in test/bake/dev/production.test.ts that builds a React Bake app with eight pages covering the require-of-ESM shapes. It touches no security-sensitive surface. The fix is small and verified live, and the prior test-speed feedback was addressed by a later commit; the earlier inline thread about the behavior change for never-executed or try/catch require() of an unservable ESM was resolved by the author only, and it is an intentional trade-off documented in the description rather than something a commit fixed, so a human should weigh it instead of this review approving over it.

This branch has not been deployed

No deployments
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.

1 participant