From c9999bdeaddd4b372793a56874c73def2ef1d1bf Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 18 Oct 2026 01:27:44 +0000 Subject: [PATCH 1/8] bundler: give the browser side of a server build its own runtime module A server-side build (--target=bun, --target=node, --compile) that imports an HTML file parses the HTML's scripts with the browser target, but every file linked against the single bun:runtime module at source index 0, which is the server variant. The client chunk got the server __require (import.meta.require, or a bare require with the CommonJS format that --bytecode forces), the server __using helpers, and the runtime parts that only the server side used. With --splitting the two sides shared the runtime chunk. The browser side now resolves bun:wrap to a second runtime module, the browser variant, added when the client transpiler is created (HTML imports) or when a bake production build starts. The linker picks the runtime by the file's target for wrappers, export helpers, and chunk prologues. The HTML manifest module is marked as a server-side module so it keeps the server runtime. The client transpiler uses the ESM output format, and the linker reads the format per file where it decides how an external import or a require() prints, so browser files keep import statements and __require even when the server format is CommonJS. Bake production no longer copies the server runtime chunk into the client output: the client chunks carry their own runtime. --- src/ast/base.rs | 6 +- src/bundler/Graph.rs | 9 + src/bundler/LinkerContext.rs | 235 +++++++++++------- src/bundler/LinkerGraph.rs | 63 ++++- src/bundler/OutputFile.rs | 1 - src/bundler/ParseTask.rs | 48 ++-- src/bundler/bundle_v2.rs | 119 ++++++--- src/bundler/linker_context/MetafileBuilder.rs | 8 +- .../computeCrossChunkDependencies.rs | 3 +- .../linker_context/convertStmtsForChunk.rs | 7 +- src/bundler/linker_context/doStep5.rs | 20 +- .../findAllImportedPartsInJSOrder.rs | 30 +-- .../generateChunksInParallel.rs | 7 +- .../generateCodeForFileInChunkJS.rs | 9 +- .../generateCompileResultForJSChunk.rs | 13 +- .../linker_context/mergeSmallChunks.rs | 4 +- .../linker_context/postProcessJSChunk.rs | 27 +- .../linker_context/renameSymbolsInChunk.rs | 4 +- .../linker_context/scanImportsAndExports.rs | 64 +++-- src/js_parser/p.rs | 9 +- src/runtime/bake/production/mod.rs | 33 --- test/bundler/bundler_html_server.test.ts | 166 ++++++++++++- 22 files changed, 624 insertions(+), 261 deletions(-) diff --git a/src/ast/base.rs b/src/ast/base.rs index bc971d13195f..e710aacceb0b 100644 --- a/src/ast/base.rs +++ b/src/ast/base.rs @@ -44,8 +44,10 @@ impl Index { self.0 } - /// if you are within the parser, use p.isSourceRuntime() instead, as the - /// runtime index (0) is used as the id for single-file transforms. + /// The main `bun:runtime` module. Within the parser use + /// `p.is_source_runtime()` instead: index 0 is also the id for single-file + /// transforms. Within the linker use `LinkerGraph::is_runtime`: a + /// server-side build with a browser side has a second runtime module. #[inline] pub const fn is_runtime(self) -> bool { self.0 == Self::RUNTIME.0 diff --git a/src/bundler/Graph.rs b/src/bundler/Graph.rs index b7adc95e436c..3e6228c033d7 100644 --- a/src/bundler/Graph.rs +++ b/src/bundler/Graph.rs @@ -91,6 +91,14 @@ pub struct Graph<'a> { /// /// Helps skip a loop. pub(crate) has_any_secondary_paths: bool, + + /// The `bun:runtime` module of the browser side of a server-side build + /// (HTML imports, server components). `Index::INVALID` when the build has + /// no browser side. The main runtime at `Index::RUNTIME` is the variant for + /// `transpiler.options.target`; browser code needs the browser variant (the + /// guarded `__require` shim, `__using` without native `Symbol.dispose`, + /// `__preload`). + pub(crate) browser_runtime: Index, } #[derive(Default)] @@ -179,6 +187,7 @@ impl<'a> Graph<'a> { kit_referenced_server_data: false, kit_referenced_client_data: false, has_any_secondary_paths: false, + browser_runtime: Index::INVALID, } } } diff --git a/src/bundler/LinkerContext.rs b/src/bundler/LinkerContext.rs index ea8170943457..e1c04d405488 100644 --- a/src/bundler/LinkerContext.rs +++ b/src/bundler/LinkerContext.rs @@ -71,6 +71,58 @@ pub(crate) use crate::linker_context::post_process_html_chunk::post_process_html pub(crate) use crate::linker_context::post_process_js_chunk::post_process_js_chunk; pub(crate) use crate::linker_context::rename_symbols_in_chunk::rename_symbols_in_chunk; +/// The runtime exports the linker refers to by name while it generates +/// wrappers and chunk prologues. One set per `bun:runtime` module in the graph. +#[derive(Clone, Copy)] +pub(crate) struct RuntimeRefs { + /// The source index of the runtime module these refs come from. + pub(crate) source_index: Index, + /// `__commonJS` + pub(crate) cjs: Ref, + /// `__esm` + pub(crate) esm: Ref, + /// `__promiseAll` + pub(crate) promise_all: Ref, + /// `__preload` / `__chunks`: modulepreload for split browser `import()`s. + /// `Ref::NONE` in the non-browser runtime variants. + pub(crate) preload: Ref, + pub(crate) chunks: Ref, +} + +impl RuntimeRefs { + pub(crate) const NONE: RuntimeRefs = RuntimeRefs { + source_index: Index::INVALID, + cjs: Ref::NONE, + esm: Ref::NONE, + promise_all: Ref::NONE, + preload: Ref::NONE, + chunks: Ref::NONE, + }; + + fn load(graph: &LinkerGraph, source_index: Index) -> RuntimeRefs { + let named_exports = &graph.ast.items_named_exports()[source_index.get() as usize]; + let required = |name: &[u8]| { + named_exports + .get(name) + .expect("infallible: runtime export") + .ref_ + }; + let optional = |name: &[u8]| { + named_exports + .get(name) + .map_or(Ref::NONE, |export| export.ref_) + }; + RuntimeRefs { + source_index, + cjs: required(b"__commonJS"), + esm: required(b"__esm"), + promise_all: required(b"__promiseAll"), + preload: optional(b"__preload"), + chunks: optional(b"__chunks"), + } + } +} + pub struct LinkerContext<'a> { pub(crate) parse_graph: *mut Graph<'a>, pub graph: LinkerGraph<'a>, @@ -89,19 +141,16 @@ pub struct LinkerContext<'a> { pub(crate) resolver: Option>>, pub(crate) cycle_detector: Vec, - /// We may need to refer to the "__esm" and/or "__commonJS" runtime symbols - pub(crate) cjs_runtime_ref: Ref, - pub(crate) esm_runtime_ref: Ref, + /// Symbols of the main runtime (`Index::RUNTIME`). Use + /// [`Self::runtime_refs`] to pick the runtime a file links against. + pub(crate) main_runtime: RuntimeRefs, + /// Symbols of the browser runtime (`LinkerGraph::browser_runtime`), when + /// the build has one. + pub(crate) browser_runtime: RuntimeRefs, /// We may need to refer to the CommonJS "module" symbol for exports pub(crate) unbound_module_ref: Ref, - /// We may need to refer to the "__promiseAll" runtime symbol - pub(crate) promise_all_runtime_ref: Ref, - /// `__preload` / `__chunks`: modulepreload for split browser `import()`s. - pub(crate) preload_runtime_ref: Ref, - pub(crate) chunks_runtime_ref: Ref, - pub(crate) options: LinkerOptions, pub(crate) r#loop: EventLoop, @@ -161,12 +210,9 @@ impl<'a> Default for LinkerContext<'a> { log: core::ptr::null_mut(), resolver: None, cycle_detector: Vec::new(), - cjs_runtime_ref: Ref::NONE, - esm_runtime_ref: Ref::NONE, + main_runtime: RuntimeRefs::NONE, + browser_runtime: RuntimeRefs::NONE, unbound_module_ref: Ref::NONE, - promise_all_runtime_ref: Ref::NONE, - preload_runtime_ref: Ref::NONE, - chunks_runtime_ref: Ref::NONE, options: Default::default(), r#loop: None, unique_key_buf: Box::default(), @@ -356,7 +402,42 @@ impl<'a> LinkerContext<'a> { && self.graph.code_splitting && self.options.target == Target::Browser && self.options.output_format == Format::Esm - && self.chunks_runtime_ref.is_valid() + && self.main_runtime.chunks.is_valid() + } + + /// The runtime symbols that the code generated for `source_index` links + /// against. See `LinkerGraph::runtime_index`. + #[inline] + pub(crate) fn runtime_refs(&self, source_index: u32) -> &RuntimeRefs { + self.runtime_refs_for_target(self.graph.ast.items_target()[source_index as usize]) + } + + /// See [`Self::runtime_refs`]. + #[inline] + pub(crate) fn runtime_refs_for_target(&self, target: Target) -> &RuntimeRefs { + if self.graph.runtime_index_for_target(target) == Index::RUNTIME { + &self.main_runtime + } else { + &self.browser_runtime + } + } + + /// The output format of the chunk that holds `source_index`. The browser + /// side of a server-side build is always ESM (see + /// `BundleV2::initialize_client_transpiler`), whatever the server format. + #[inline] + pub(crate) fn output_format_for(&self, source_index: u32) -> Format { + self.output_format_for_target(self.graph.ast.items_target()[source_index as usize]) + } + + /// See [`Self::output_format_for`]. + #[inline] + pub(crate) fn output_format_for_target(&self, target: Target) -> Format { + if self.graph.runtime_index_for_target(target) == Index::RUNTIME { + self.options.output_format + } else { + Format::Esm + } } /// An `import()` — or a split `require()` — whose target is a @@ -552,6 +633,8 @@ impl<'a> LinkerContext<'a> { // the bundle's lifetime; `resolver`/`log`/`options` are stable fields. let transpiler = unsafe { &mut *(*bundle).transpiler }; self.graph.code_splitting = transpiler.options.code_splitting; + // SAFETY: see `parse_graph` above; scalar read. + self.graph.browser_runtime = unsafe { (*self.parse_graph).browser_runtime }; // `transpiler.log` is the canonical // `*mut Log` (same value aliased into `linker.log` / `resolver.log`). self.log = transpiler.log; @@ -586,28 +669,10 @@ impl<'a> LinkerContext<'a> { )?; dyn_entry_points.clear_retaining_capacity(); - let runtime_named_exports = - &self.graph.ast.items_named_exports()[Index::RUNTIME.get() as usize]; - - self.esm_runtime_ref = runtime_named_exports - .get(b"__esm") - .expect("infallible: runtime export") - .ref_; - self.cjs_runtime_ref = runtime_named_exports - .get(b"__commonJS") - .expect("infallible: runtime export") - .ref_; - self.promise_all_runtime_ref = runtime_named_exports - .get(b"__promiseAll") - .expect("infallible: runtime export") - .ref_; - // Browser runtime only (`RUNTIME_PRELOAD_BROWSER`). - self.preload_runtime_ref = runtime_named_exports - .get(b"__preload") - .map_or(Ref::NONE, |export| export.ref_); - self.chunks_runtime_ref = runtime_named_exports - .get(b"__chunks") - .map_or(Ref::NONE, |export| export.ref_); + self.main_runtime = RuntimeRefs::load(&self.graph, Index::RUNTIME); + if self.graph.browser_runtime.is_valid() { + self.browser_runtime = RuntimeRefs::load(&self.graph, self.graph.browser_runtime); + } if self.options.output_format == Format::Cjs { self.unbound_module_ref = self.graph.generate_new_symbol( @@ -739,8 +804,6 @@ impl<'a> LinkerContext<'a> { // SAFETY: see above; sole accessor of `html_imports` for this scope. let server_len = unsafe { (*parse_graph).html_imports.server_source_indices.len() }; if server_len > 0 { - let actual_ref = self.graph.runtime_function(b"__jsonParse"); - for i in 0..server_len as usize { // SAFETY: `server_source_indices` is a stable Vec; index // bounded by `server_len`. @@ -785,6 +848,8 @@ impl<'a> LinkerContext<'a> { .ref_; // Make the __jsonParse in that file point to the __jsonParse in the runtime chunk. + let runtime = self.graph.runtime_index(html_import); + let actual_ref = self.graph.runtime_function(html_import, b"__jsonParse"); // SAFETY: `original_ref`'s symbol slot is disjoint from any live borrow here // (only `actual_ref` is held, a `Copy` value). unsafe { self.graph.symbol_mut(original_ref) } @@ -798,7 +863,7 @@ impl<'a> LinkerContext<'a> { Index::part(1u32).get(), actual_ref, 1, - Index::RUNTIME, + runtime, ) .expect("OOM"); } @@ -907,7 +972,7 @@ impl<'a> LinkerContext<'a> { let mut source_index: u32 = 0; while (source_index as usize) < files_len { // Skip runtime - if source_index == Index::RUNTIME.get() { + if self.graph.is_runtime(source_index) { source_index += 1; continue; } @@ -2266,7 +2331,10 @@ impl<'a> LinkerContext<'a> { // Is this an external import? if !record.source_index.is_valid() { // Keep the "import" statement if import statements are supported - if self.options.output_format.keep_es6_import_export_syntax() { + if self + .output_format_for_target(ast.target) + .keep_es6_import_export_syntax() + { return Ok(false); } @@ -2369,9 +2437,10 @@ impl<'a> LinkerContext<'a> { ); if other_flags.is_async_or_has_async_dependency { + let promise_all = self.runtime_refs_for_target(ast.target).promise_all; stmts .inside_wrapper_prefix - .append_async_dependency(init_call, self.promise_all_runtime_ref)?; + .append_async_dependency(init_call, promise_all)?; } else { stmts .inside_wrapper_prefix @@ -2458,18 +2527,18 @@ impl<'a> LinkerContext<'a> { minify_whitespace: self.options.minify_whitespace, minify_syntax: self.options.minify_syntax, input_module_type: ast.module_type, - module_type: self.options.output_format, + module_type: self.output_format_for(source_index.get()), print_dce_annotations: self.options.emit_dce_annotations, has_run_symbol_renamer: true, to_esm_ref, to_commonjs_ref, module_preload_ref: if self.module_preload() { - self.preload_runtime_ref + self.runtime_refs(source_index.get()).preload } else { Ref::NONE }, - require_ref: match self.options.output_format { + require_ref: match self.output_format_for(source_index.get()) { Format::Cjs => None, // use unbounded global _ => runtime_require_ref, }, @@ -2521,8 +2590,8 @@ impl<'a> LinkerContext<'a> { core::mem::transmute::, renamer::Renamer<'_, '_>>(r) }; - let enable_source_maps = - self.options.source_maps != SourceMapOption::None && !source_index.is_runtime(); + let enable_source_maps = self.options.source_maps != SourceMapOption::None + && !self.graph.is_runtime(source_index.get()); let result = if enable_source_maps { js_printer::print_with_writer::<&mut js_printer::BufferPrinter, true>( &mut printer, @@ -2974,6 +3043,7 @@ impl<'a> LinkerContext<'a> { } preload_entries.set(id); let part_index = self.entry_point_part_indices[id]; + let runtime = *self.runtime_refs(entry); // Through `ctx.parts` (the tree shaker's view of the parts column), not a second `&mut` via `self.graph`. { let ast = self.graph.ast.split_raw(); @@ -2999,9 +3069,9 @@ impl<'a> LinkerContext<'a> { overlay, entry, part_index, - self.chunks_runtime_ref, + runtime.chunks, 1, - Index::RUNTIME, + runtime.source_index, )?; } if ctx.parts_live[id].is_set(part_index as usize) { @@ -3323,10 +3393,11 @@ fn import_tracker_eq(a: &ImportTracker, b: &ImportTracker) -> bool { } impl<'a> LinkerContext<'a> { - /// Looks up the symbol `Ref` for a named export of the runtime module. + /// Looks up the symbol `Ref` for a named export of the runtime module + /// that `source_index` links against. #[inline] - pub(crate) fn runtime_function(&self, name: &[u8]) -> Ref { - self.graph.runtime_function(name) + pub(crate) fn runtime_function(&self, source_index: u32, name: &[u8]) -> Ref { + self.graph.runtime_function(source_index, name) } /// Returns the part indices within file `id` that declare the @@ -3336,11 +3407,15 @@ impl<'a> LinkerContext<'a> { self.graph.top_level_symbol_to_parts(id, r#ref) } - /// Returns the part indices in the runtime module that declare the - /// top-level symbol `ref`. + /// Returns the part indices in the runtime module `runtime` that declare + /// the top-level symbol `ref`. #[inline] - pub(crate) fn top_level_symbols_to_parts_for_runtime(&self, r#ref: Ref) -> &[u32] { - self.top_level_symbols_to_parts(Index::RUNTIME.get(), r#ref) + pub(crate) fn top_level_symbols_to_parts_for_runtime( + &self, + runtime: Index, + r#ref: Ref, + ) -> &[u32] { + self.top_level_symbols_to_parts(runtime.get(), r#ref) } /// Note: returns `'static` so callers can hold the source across a @@ -3438,6 +3513,7 @@ impl<'a> LinkerContext<'a> { wrapper_part_index: &mut crate::Index, source_index: crate::IndexInt, ) { + let runtime = *self.runtime_refs(source_index); match wrap { // If this is a CommonJS file, we're going to need to generate a wrapper // for the CommonJS closure. That will end up looking something like this: @@ -3455,21 +3531,7 @@ impl<'a> LinkerContext<'a> { // of it. WrapKind::Cjs => { let common_js_parts = - self.top_level_symbols_to_parts_for_runtime(self.cjs_runtime_ref); - - // Note: the inner loop is intentionally a no-op - // (`if r#ref.eql(...) continue;` only). - for &part_id in common_js_parts { - let runtime_parts = - self.graph.ast.items_parts()[Index::RUNTIME.get() as usize].as_slice(); - let part: &Part = &runtime_parts[part_id as usize]; - let symbol_refs = part.symbol_uses.keys(); - for r#ref in symbol_refs { - if *r#ref == self.cjs_runtime_ref { - continue; - } - } - } + self.top_level_symbols_to_parts_for_runtime(runtime.source_index, runtime.cjs); // generate a dummy part that depends on the "__commonJS" symbol. let dependencies: DependencyList = @@ -3478,7 +3540,7 @@ impl<'a> LinkerContext<'a> { for &part in common_js_parts { deps.append_assume_capacity(Dependency { part_index: part, - source_index: bun_ast::Index::RUNTIME, + source_index: runtime.source_index, }); } deps @@ -3527,9 +3589,9 @@ impl<'a> LinkerContext<'a> { .generate_symbol_import_and_use( source_index, part_index, - self.cjs_runtime_ref, + runtime.cjs, 1, - crate::Index::RUNTIME, + runtime.source_index, ) .expect("unreachable"); } @@ -3573,7 +3635,7 @@ impl<'a> LinkerContext<'a> { let esm_parts: &[u32] = if wrapper_ref.is_valid() && self.options.output_format != Format::InternalBakeDev { - self.top_level_symbols_to_parts_for_runtime(self.esm_runtime_ref) + self.top_level_symbols_to_parts_for_runtime(runtime.source_index, runtime.esm) } else { &[] }; @@ -3582,7 +3644,10 @@ impl<'a> LinkerContext<'a> { && wrapper_ref.is_valid() && self.options.output_format != Format::InternalBakeDev { - self.top_level_symbols_to_parts_for_runtime(self.promise_all_runtime_ref) + self.top_level_symbols_to_parts_for_runtime( + runtime.source_index, + runtime.promise_all, + ) } else { &[] }; @@ -3593,13 +3658,13 @@ impl<'a> LinkerContext<'a> { for &part in esm_parts { dependencies.append_assume_capacity(Dependency { part_index: part, - source_index: bun_ast::Index::RUNTIME, + source_index: runtime.source_index, }); } for &part in promise_all_parts { dependencies.append_assume_capacity(Dependency { part_index: part, - source_index: bun_ast::Index::RUNTIME, + source_index: runtime.source_index, }); } @@ -3630,9 +3695,9 @@ impl<'a> LinkerContext<'a> { .generate_symbol_import_and_use( source_index, part_index, - self.esm_runtime_ref, + runtime.esm, 1, - crate::Index::RUNTIME, + runtime.source_index, ) .expect("OOM"); @@ -3642,9 +3707,9 @@ impl<'a> LinkerContext<'a> { .generate_symbol_import_and_use( source_index, part_index, - self.promise_all_runtime_ref, + runtime.promise_all, 1, - crate::Index::RUNTIME, + runtime.source_index, ) .expect("OOM"); } @@ -3988,7 +4053,9 @@ impl<'a> LinkerContext<'a> { | ImportTrackerStatus::Disabled | ImportTrackerStatus::External => { if status == ImportTrackerStatus::External - && self.options.output_format.keep_es6_import_export_syntax() + && self + .output_format_for(prev_source_index) + .keep_es6_import_export_syntax() { // Imports from external modules should not be converted to CommonJS // if the output format preserves the original ES6 import statements diff --git a/src/bundler/LinkerGraph.rs b/src/bundler/LinkerGraph.rs index 8379670d81ff..a5c85453d3cf 100644 --- a/src/bundler/LinkerGraph.rs +++ b/src/bundler/LinkerGraph.rs @@ -234,6 +234,9 @@ pub struct LinkerGraph<'a> { pub(crate) code_splitting: bool, + /// See `Graph::browser_runtime`. Copied from the parse graph in `link`. + pub(crate) browser_runtime: Index, + // This is an alias from Graph // it is not a clone! pub(crate) ast: MultiArrayList>, @@ -312,6 +315,7 @@ impl Default for LinkerGraph<'_> { // dangling sentinel (never read before assignment). bump: bun_ptr::BackRef::from(core::ptr::NonNull::dangling()), code_splitting: false, + browser_runtime: Index::INVALID, ast: MultiArrayList::default(), meta: MultiArrayList::default(), reachable_files: Vec::new(), @@ -334,8 +338,12 @@ impl Default for LinkerGraph<'_> { // thin forwarders for call sites that don't have a split in hand. // ────────────────────────────────────────────────────────────────────────── -fn runtime_function(named_exports: &[bundled_ast::NamedExports], name: &[u8]) -> Ref { - named_exports[Index::RUNTIME.get() as usize] +fn runtime_function( + named_exports: &[bundled_ast::NamedExports], + runtime: Index, + name: &[u8], +) -> Ref { + named_exports[runtime.get() as usize] .get(name) .expect("runtime function must be a named export of the runtime module") .ref_ @@ -518,8 +526,50 @@ pub(crate) fn generate_symbol_import_and_use( } impl<'a> LinkerGraph<'a> { - pub(crate) fn runtime_function(&self, name: &[u8]) -> Ref { - runtime_function(self.ast.items_named_exports(), name) + /// Whether `source_index` is one of the `bun:runtime` modules. + #[inline] + pub(crate) fn is_runtime(&self, source_index: index::Int) -> bool { + source_index == Index::RUNTIME.get() + || (self.browser_runtime.is_valid() && source_index == self.browser_runtime.get()) + } + + /// Every `bun:runtime` module in the graph, the main one first. + #[inline] + pub(crate) fn runtimes(&self) -> impl Iterator { + core::iter::once(Index::RUNTIME.get()).chain( + self.browser_runtime + .is_valid() + .then(|| self.browser_runtime.get()), + ) + } + + /// The `bun:runtime` module that the code generated for `source_index` + /// links against: the browser runtime for a browser-side file of a + /// server-side build, the main runtime otherwise. Mirrors + /// `BundleV2::runtime_source_index`, which resolved the file's own + /// `bun:wrap` import records. + #[inline] + pub(crate) fn runtime_index(&self, source_index: index::Int) -> Index { + self.runtime_index_for_target(self.ast.items_target()[source_index as usize]) + } + + /// See [`Self::runtime_index`]. + #[inline] + pub(crate) fn runtime_index_for_target(&self, target: bun_ast::Target) -> Index { + if self.browser_runtime.is_valid() && target == bun_ast::Target::Browser { + self.browser_runtime + } else { + Index::RUNTIME + } + } + + /// The symbol for the runtime export `name` that `source_index` links against. + pub(crate) fn runtime_function(&self, source_index: index::Int, name: &[u8]) -> Ref { + runtime_function( + self.ast.items_named_exports(), + self.runtime_index(source_index), + name, + ) } /// Shared-ref view of a symbol that is known to exist (the `Ref` was @@ -592,13 +642,14 @@ impl<'a> LinkerGraph<'a> { source_index ); - let ref_ = self.runtime_function(name); + let runtime = self.runtime_index(source_index); + let ref_ = runtime_function(self.ast.items_named_exports(), runtime, name); self.generate_symbol_import_and_use( source_index, entry_point_part_index.get(), ref_, count, - Index::RUNTIME, + runtime, ) } diff --git a/src/bundler/OutputFile.rs b/src/bundler/OutputFile.rs index e1f8bcc1eb96..b8b6ef1b1e69 100644 --- a/src/bundler/OutputFile.rs +++ b/src/bundler/OutputFile.rs @@ -81,7 +81,6 @@ impl OutputFile { #[derive(Default, Clone, Copy)] pub struct BakeExtra { pub route: BakeRouteKind, - pub bake_is_runtime: bool, } #[derive(Default, Clone, Copy, PartialEq, Eq)] diff --git a/src/bundler/ParseTask.rs b/src/bundler/ParseTask.rs index 1c20cf221d97..c83995df1077 100644 --- a/src/bundler/ParseTask.rs +++ b/src/bundler/ParseTask.rs @@ -319,11 +319,25 @@ impl ParseTask { /// Re-export of `parse_worker::get_runtime_source` as an associated fn so /// callers can spell it `ParseTask::get_runtime_source`. #[inline] - pub(crate) fn get_runtime_source(target: options::Target) -> RuntimeSource { - parse_worker::get_runtime_source(target) + pub(crate) fn get_runtime_source( + target: options::Target, + source_index: Index, + ) -> RuntimeSource { + parse_worker::get_runtime_source(target, source_index) + } + + /// The task parses a `bun:runtime` module. A server-side build with a + /// browser side has two of them (see `BundleV2::runtime_source_index`), so + /// this is not the same as `source_index.is_runtime()`. + #[inline] + pub(crate) fn is_runtime(&self) -> bool { + self.path.namespace == RUNTIME_NAMESPACE } } +/// `Path.namespace` of every `bun:runtime` source. +pub(crate) const RUNTIME_NAMESPACE: &[u8] = b"bun:runtime"; + impl Default for ParseTask { fn default() -> Self { ParseTask { @@ -567,7 +581,7 @@ export var __callDispose = (stack, error, hasError) => { pub mod parse_worker { use super::*; - fn get_runtime_source_comptime(target: options::Target) -> RuntimeSource { + fn get_runtime_source_comptime(target: options::Target, source_index: Index) -> RuntimeSource { // The runtime module is the shared `runtime.js` body plus a per-target // `__require`/`__using` tail. Concatenating at compile time would embed // four copies of the 13 KB body, so each variant is assembled once on @@ -610,14 +624,14 @@ pub mod parse_worker { let parse_task = ParseTask { ctx: None, - path: Fs::Path::init_with_namespace(b"runtime", b"bun:runtime"), + path: Fs::Path::init_with_namespace(b"runtime", RUNTIME_NAMESPACE), side_effects: bun_ast::SideEffects::NoSideEffectsPureData, jsx: options::jsx::Pragma { parse: false, ..Default::default() }, contents_or_fd: ContentsOrFd::Contents(runtime_code), - source_index: Index::RUNTIME, + source_index, loader: Some(Loader::Js), known_target: target, // defaults: @@ -647,22 +661,23 @@ pub mod parse_worker { // directly rather than `clone()` across the type boundary. path: bun_paths::fs::Path { text: b"runtime", - namespace: b"bun:runtime", + namespace: RUNTIME_NAMESPACE, pretty: b"", is_disabled: false, is_symlink: false, }, contents: std::borrow::Cow::Borrowed(runtime_code), - // `Source.index` is `bun_ast::Index` (newtype `u32`), - // distinct from `bun_ast::Index`. Runtime source is index 0. - index: bun_ast::Index(Index::RUNTIME.get()), + index: bun_ast::Index(source_index.get()), ..Default::default() }; RuntimeSource { parse_task, source } } - pub(crate) fn get_runtime_source(target: options::Target) -> RuntimeSource { - get_runtime_source_comptime(target) + pub(crate) fn get_runtime_source( + target: options::Target, + source_index: Index, + ) -> RuntimeSource { + get_runtime_source_comptime(target, source_index) } // ─────────────────────────────────────────────────────────────────────────── @@ -1629,7 +1644,7 @@ pub mod parse_worker { from_plugin: &mut bool, ) -> core::result::Result { let might_have_on_parse_plugins = 'brk: { - if task.source_index.is_runtime() { + if task.is_runtime() { break 'brk false; } // SAFETY: ctx backref is valid for the bundle pass (outlives `'r`). @@ -2518,7 +2533,7 @@ pub mod parse_worker { }; opts.package_version = task.package_version.slice(); - opts.features.allow_runtime = !task.source_index.is_runtime(); + opts.features.allow_runtime = !task.is_runtime(); opts.features.unwrap_commonjs_to_esm = output_format == options::Format::Esm && FeatureFlags::UNWRAP_COMMONJS_TO_ESM; opts.features.top_level_await = output_format == options::Format::Esm @@ -2561,7 +2576,7 @@ pub mod parse_worker { // targeting Bun there is no need to lower them. opts.features.lower_using = !target.is_bun(); opts.features.hot_module_reloading = - output_format == options::Format::InternalBakeDev && !task.source_index.is_runtime(); + output_format == options::Format::InternalBakeDev && !task.is_runtime(); opts.features.auto_polyfill_require = output_format == options::Format::Esm && !opts.features.hot_module_reloading; opts.features.react_fast_refresh = @@ -2678,8 +2693,7 @@ pub mod parse_worker { } } - opts.ignore_dce_annotations = - topts.ignore_dce_annotations && !task.source_index.is_runtime(); + opts.ignore_dce_annotations = topts.ignore_dce_annotations && !task.is_runtime(); // For files that are not user-specified entrypoints, set `import.meta.main` to `false`. // Entrypoints will have `import.meta.main` set as "unknown", unless we use `--compile`, @@ -2690,7 +2704,7 @@ pub mod parse_worker { opts.lower_import_meta_main_for_node_js = true; } - opts.tree_shaking = if task.source_index.is_runtime() { + opts.tree_shaking = if task.is_runtime() { true } else { topts.tree_shaking diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index eb72b4d389c5..fcc8e8136308 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -1999,6 +1999,9 @@ pub mod bv2_impl { unsafe { Transpiler::for_worker(this_transpiler, arena, this_transpiler.log) }; ct.options.target = Target::Browser; + // The client assets of an HTML import are ES modules whatever the + // server bundle's format is (`--compile --bytecode` forces CommonJS). + ct.options.output_format = options::Format::Esm; // Don't inherit SSR mode from the server target: the SSR pass // drops hook setter bindings, which is invalid for browser code. if ct.options.react_compiler.is_ssr() { @@ -2055,7 +2058,9 @@ pub mod bv2_impl { self.owned_client_transpiler = Some(boxed); let ct: &mut Transpiler<'a> = self.owned_client_transpiler.as_deref_mut().unwrap(); self.client_transpiler = Some(bun_ptr::ParentRef::from_ref_mut(&mut *ct)); - Ok(ct) + // The browser side starts here, and its files link against their own runtime. + self.ensure_browser_runtime()?; + Ok(self.owned_client_transpiler.as_deref_mut().unwrap()) } /// By calling this function, it implies that the returned log *will* be @@ -2400,6 +2405,9 @@ pub mod bv2_impl { // If we don't include the runtime, __toESM or __toCommonJS will not get // imported and weird things will happen visitor.visit::(Index::RUNTIME, false); + if self.graph.browser_runtime.is_valid() { + visitor.visit::(self.graph.browser_runtime, false); + } if self.transpiler.options.code_splitting { for entry_point in self.graph.entry_points.iter().copied() { @@ -3766,6 +3774,7 @@ pub mod bv2_impl { ) -> Result<(), Error> { self.enqueue_entry_points_common()?; self.reserve_source_indexes_for_bake()?; + self.ensure_browser_runtime()?; let num_entry_points = data.files.count(); self.graph.entry_points.reserve(num_entry_points); @@ -3800,19 +3809,25 @@ pub mod bv2_impl { /// Common prelude shared by all enqueue_entry_points_* variants: add the runtime task. fn enqueue_entry_points_common(&mut self) -> Result<(), Error> { - // Add the runtime - let rt = ParseTask::get_runtime_source(self.transpiler.options.target); + debug_assert!(self.graph.input_files.len() == 0); + self.enqueue_runtime(self.transpiler.options.target)?; + Ok(()) + } + + /// Adds the `bun:runtime` module for `target` as the next source index + /// and maps `bun:wrap` to it in that target's path map. + fn enqueue_runtime(&mut self, target: options::Target) -> Result { + let source_index = Index::source(self.graph.input_files.len() as IndexInt); + let rt = ParseTask::get_runtime_source(target, source_index); self.graph.input_files.append(crate::Graph::InputFile { source: rt.source, loader: Loader::Js, side_effects: bun_ast::SideEffects::NoSideEffectsPureData, ..Default::default() })?; - - // try this.graph.entry_points.append(arena, Index.runtime); let _ = self.graph.ast.append(JSAst::empty_in(self.graph.heap)); // OOM/capacity: fire-and-forget - self.path_to_source_index_map(self.transpiler.options.target) - .put(&b"bun:wrap"[..], Index::RUNTIME.get()) + self.path_to_source_index_map(target) + .put(&b"bun:wrap"[..], source_index.get()) .expect("oom"); // SAFETY: arena (`self.graph.heap`) outlives the bundle pass; coerce the // `&mut ParseTask` to `*mut` immediately so the `&self` borrow from @@ -3829,9 +3844,41 @@ pub mod bv2_impl { } self.increment_scan_counter(); self.graph.pool().schedule(runtime_parse_task); + Ok(source_index) + } + + /// Whether the browser side of this server-side build has its own + /// `bun:runtime` module (the browser variant). The dev server format + /// keeps the single one: its module registry provides `bun:wrap`, and + /// the printer knows the runtime as source index 0. + fn has_browser_runtime(&self) -> bool { + self.transpiler.options.target.is_server_side() + && self.dev_server.is_none() + && self.transpiler.options.output_format != options::Format::InternalBakeDev + } + + /// Adds the browser runtime once the build has a browser side: when the + /// client transpiler is created for an HTML import, or when a bake + /// production build starts with client entry points. Every browser file + /// is enqueued after this, so `runtime_source_index` never misses it. + pub(crate) fn ensure_browser_runtime(&mut self) -> Result<(), Error> { + if self.has_browser_runtime() && !self.graph.browser_runtime.is_valid() { + self.graph.browser_runtime = self.enqueue_runtime(Target::Browser)?; + } Ok(()) } + /// The `bun:runtime` module that a file parsed for `target` links against. + pub(crate) fn runtime_source_index(&self, target: options::Target) -> Index { + if target == Target::Browser && self.has_browser_runtime() { + debug_assert!(self.graph.browser_runtime.is_valid()); + if self.graph.browser_runtime.is_valid() { + return self.graph.browser_runtime; + } + } + Index::RUNTIME + } + fn clone_ast(&mut self) -> Result<(), Error> { let _trace = crate::perf::trace("Bundler.cloneAST"); self.linker.graph.ast = self.graph.ast.clone()?; @@ -4491,7 +4538,10 @@ pub mod bv2_impl { this.process_server_component_manifest_files()?; let reachable_files = this.find_reachable_files()?; - *reachable_files_count = reachable_files.len().saturating_sub(1); // - 1 for the runtime + // Without the runtime modules. + *reachable_files_count = reachable_files + .len() + .saturating_sub(1 + usize::from(this.graph.browser_runtime.is_valid())); this.process_files_to_copy(&reachable_files)?; @@ -6665,13 +6715,14 @@ pub mod bv2_impl { let only_records = ctx.only_records; debug_assert!(only_records.is_none_or(<[u32]>::is_sorted)); let mut estimated_resolve_queue_count: usize = 0; + let runtime_source_index = self.runtime_source_index(ctx.target); for import_record in ctx.import_records.iter_mut() { if import_record .flags .contains(bun_ast::ImportRecordFlags::IS_INTERNAL) { import_record.tag = bun_ast::ImportRecordTag::Runtime; - import_record.source_index = Index::RUNTIME; + import_record.source_index = runtime_source_index; } // For non-dev-server builds, barrel-deferred records need their @@ -6760,7 +6811,7 @@ pub mod bv2_impl { import_record.path.namespace = b"bun"; import_record.tag = bun_ast::ImportRecordTag::Runtime; import_record.path.text = b"wrap"; - import_record.source_index = Index::RUNTIME; + import_record.source_index = runtime_source_index; continue; } @@ -7630,27 +7681,29 @@ pub mod bv2_impl { (&raw mut *transpiler.options.define, transpiler.log) }; - let ast_for_html_entrypoint = JSAst::init( - bun_js_parser::new_lazy_export_ast( - heap, - // SAFETY: `define`/`log` live for `'a` (owned by the Transpiler). - unsafe { &mut *define_ptr }, - js_parser_options, - // SAFETY: `define`/`log` live for `'a` (owned by the Transpiler). - unsafe { &mut *log_ptr }, - Expr::init( - E::EString { - data: unique_key.into(), - ..Default::default() - }, - bun_ast::Loc::EMPTY, - ), - empty_html_file_source, - // We replace this runtime API call's ref later via .link on the Symbol. - b"__jsonParse", - )? - .ok_or(Error::ParserError)?, - ); + let mut manifest_ast = bun_js_parser::new_lazy_export_ast( + heap, + // SAFETY: `define`/`log` live for `'a` (owned by the Transpiler). + unsafe { &mut *define_ptr }, + js_parser_options, + // SAFETY: `define`/`log` live for `'a` (owned by the Transpiler). + unsafe { &mut *log_ptr }, + Expr::init( + E::EString { + data: unique_key.into(), + ..Default::default() + }, + bun_ast::Loc::EMPTY, + ), + empty_html_file_source, + // We replace this runtime API call's ref later via .link on the Symbol. + b"__jsonParse", + )? + .ok_or(Error::ParserError)?; + // The manifest is a server-side module: it links against the + // server runtime's `__jsonParse` and lands in the server chunk. + manifest_ast.target = target; + let ast_for_html_entrypoint = JSAst::init(manifest_ast); let fake_input_file = crate::Graph::InputFile { source: empty_html_file_source.clone(), @@ -7812,7 +7865,9 @@ pub mod bv2_impl { [result_source_index] .path .text; - this.source_code_length += if result_source_index != 0 { + this.source_code_length += if result_source_index != 0 + && result_source_index != this.graph.browser_runtime.get() as usize + { this.graph.input_files.items_source()[result_source_index] .contents .len() diff --git a/src/bundler/linker_context/MetafileBuilder.rs b/src/bundler/linker_context/MetafileBuilder.rs index 10c528d91108..bca9a947e12e 100644 --- a/src/bundler/linker_context/MetafileBuilder.rs +++ b/src/bundler/linker_context/MetafileBuilder.rs @@ -46,7 +46,7 @@ use bun_ast::ImportRecordFlags; use crate::chunk::{Content as ChunkContent, ReferencePathStyle, SourceMapShiftTracking}; use crate::options::Loader; -use crate::{Chunk, Index, LinkerContext}; +use crate::{Chunk, LinkerContext}; #[inline] fn fmt_size(bytes: u64) -> bfmt::SizeFormatter { @@ -87,7 +87,7 @@ fn generate_chunk_json( if file_source_index as usize >= sources.len() { continue; } - if file_source_index == Index::RUNTIME.get() { + if c.graph.is_runtime(file_source_index) { continue; } @@ -256,7 +256,7 @@ pub(crate) fn generate(c: &mut LinkerContext, chunks: &mut [Chunk]) -> crate::Re } // Skip runtime and other special files - if source_index == Index::RUNTIME.get() { + if c.graph.is_runtime(source_index) { continue; } @@ -312,7 +312,7 @@ pub(crate) fn generate(c: &mut LinkerContext, chunks: &mut [Chunk]) -> crate::Re // `source_index` without rewriting the path. Externals/chunk refs fall through. let import_path: &[u8] = 'path: { if record.source_index.is_valid() - && record.source_index.get() != Index::RUNTIME.get() + && !c.graph.is_runtime(record.source_index.get()) { let idx = record.source_index.get() as usize; if idx < sources.len() { diff --git a/src/bundler/linker_context/computeCrossChunkDependencies.rs b/src/bundler/linker_context/computeCrossChunkDependencies.rs index d93259631d11..27c4cb7c5bef 100644 --- a/src/bundler/linker_context/computeCrossChunkDependencies.rs +++ b/src/bundler/linker_context/computeCrossChunkDependencies.rs @@ -318,7 +318,8 @@ impl<'a, 'bump> CrossChunkDependencies<'a, 'bump> { .preload_entries .is_set(chunk.entry_point.source_index() as usize) { - let chunks_ref = symbols.follow(ctx.chunks_runtime_ref); + let chunks_ref = + symbols.follow(ctx.runtime_refs(chunk.entry_point.source_index()).chunks); let _ = chunk_meta.imports.put(chunks_ref, ()); // OOM-only Result } diff --git a/src/bundler/linker_context/convertStmtsForChunk.rs b/src/bundler/linker_context/convertStmtsForChunk.rs index f9e72a5ae5ec..e9cfc4c4d310 100644 --- a/src/bundler/linker_context/convertStmtsForChunk.rs +++ b/src/bundler/linker_context/convertStmtsForChunk.rs @@ -170,7 +170,8 @@ pub(crate) fn convert_stmts_for_chunk( // Is this export star evaluated at run time? if !record.source_index.is_valid() - && c.options.output_format.keep_es6_import_export_syntax() + && c.output_format_for_target(ast.target) + .keep_es6_import_export_syntax() { if is_module_exports_of_wrapped_file { stmts.append( @@ -210,7 +211,7 @@ pub(crate) fn convert_stmts_for_chunk( ); // Prefix this module with "__reExport(exports, ns, module.exports)" - let export_star_ref = c.runtime_function(b"__reExport"); + let export_star_ref = c.runtime_function(source_index, b"__reExport"); let args_len = 2 + usize::from(module_exports_for_export.is_some()); let mut args: Vec = Vec::with_capacity(args_len); args.push(Expr::init( @@ -350,7 +351,7 @@ pub(crate) fn convert_stmts_for_chunk( }; // Prefix this module with "__reExport(exports, require(path), module.exports)" - let export_star_ref = c.runtime_function(b"__reExport"); + let export_star_ref = c.runtime_function(source_index, b"__reExport"); let args_len = 2 + usize::from(module_exports_for_export.is_some()); let mut args: Vec = Vec::with_capacity(args_len); args.push(Expr::init( diff --git a/src/bundler/linker_context/doStep5.rs b/src/bundler/linker_context/doStep5.rs index 431b71a14ee5..f41981cd3ec9 100644 --- a/src/bundler/linker_context/doStep5.rs +++ b/src/bundler/linker_context/doStep5.rs @@ -693,11 +693,14 @@ impl LinkerContext<'_> { let mut export_ref = Ref::NONE; if !properties.is_empty() { let is_lifted = lifted_setter_param.is_valid(); - export_ref = self.runtime_function(if is_lifted { - b"__exportCjs" - } else { - self.export_runtime_function() - }); + export_ref = self.runtime_function( + id, + if is_lifted { + b"__exportCjs" + } else { + self.export_runtime_function() + }, + ); // `bumpalo::Vec` → `Vec` via the global heap; // `G::PropertyList` is `Vec` and currently has no // arena-backed `move_from_list`, so re-own. @@ -749,11 +752,12 @@ impl LinkerContext<'_> { loc, )); // Make sure this file depends on the "__export" symbol - let parts = self.top_level_symbols_to_parts_for_runtime(export_ref); + let runtime = self.graph.runtime_index(id); + let parts = self.top_level_symbols_to_parts_for_runtime(runtime, export_ref); ns_export_dependencies.ensure_unused_capacity(parts.len()); for &part_index in parts { ns_export_dependencies.append_assume_capacity(Dependency { - source_index: bun_ast::Index::RUNTIME, + source_index: runtime, part_index, }); } @@ -770,7 +774,7 @@ impl LinkerContext<'_> { // This stays the last statement of the part: `generate_code_for_file_in_chunk_js` // drops it when another entry point's chunk prints this file. if force_include_exports_for_entry_point { - let to_common_js_ref = self.runtime_function(b"__toCommonJS"); + let to_common_js_ref = self.runtime_function(id, b"__toCommonJS"); emit_export_stmt!(Stmt::assign( Expr::allocate( arena, diff --git a/src/bundler/linker_context/findAllImportedPartsInJSOrder.rs b/src/bundler/linker_context/findAllImportedPartsInJSOrder.rs index ef2459b11239..5a904405142c 100644 --- a/src/bundler/linker_context/findAllImportedPartsInJSOrder.rs +++ b/src/bundler/linker_context/findAllImportedPartsInJSOrder.rs @@ -141,16 +141,17 @@ pub(crate) fn find_imported_parts_in_js_order( }; // The runtime goes first: every helper a file calls is declared above it. - let runtime = Index::RUNTIME.value(); - if chunk.files_with_parts_in_chunk.contains(&runtime) { - layout.place(PartRun { - source_index: runtime, - begin: 0, - end: u32::MAX, - }); + for runtime in this.graph.runtimes() { + if chunk.files_with_parts_in_chunk.contains(&runtime) { + layout.place(PartRun { + source_index: runtime, + begin: 0, + end: u32::MAX, + }); + } } for &run in runs { - if run.source_index != runtime { + if !this.graph.is_runtime(run.source_index) { layout.place(run); } } @@ -453,7 +454,7 @@ impl LoadTimeReads { for_each_live_part(&mut |file, id, part| { // A wrapped file runs when it is called, and a namespace object prints ahead of every file. if flags[file].wrap == Wrap::None - && file as u32 != Index::RUNTIME.value() + && !c.graph.is_runtime(file as u32) && id - first_part_id[file] != bun_ast::NAMESPACE_EXPORT_PART_INDEX { effects[id as usize] = LoadEffect::of(part); @@ -744,7 +745,7 @@ fn load_rank(c: &LinkerContext, entry_id_of_file: &[u32]) -> Vec { loader, } => (source_index, loader), }; - if source_index == Index::RUNTIME.value() || visited.is_set(source_index as usize) { + if c.graph.is_runtime(source_index) || visited.is_set(source_index as usize) { continue; } // A load evaluates the files that its entry point loads. It goes through the others. @@ -819,11 +820,10 @@ impl EntryWalk { ); // Chunk folding can move a file into a chunk whose entry points do not import it. It goes last there. - let runtime = Index::RUNTIME.value(); for slot in 0..self.owned.len() { let chunk = &chunks[self.owned[slot].chunk_index as usize]; for &source_index in chunk.files_with_parts_in_chunk.keys() { - if source_index != runtime && !seen.is_set(source_index as usize) { + if !c.graph.is_runtime(source_index) && !seen.is_set(source_index as usize) { self.walk( c, plan, @@ -921,7 +921,7 @@ impl EntryWalk { let owned = &mut self.owned; // The parts ahead of the one that imports `other` print before `other` does. let mut import = |part_index: u32, other: IndexInt, loader: u32| { - if other == Index::RUNTIME.value() { + if c.graph.is_runtime(other) { return; } if seen.is_set(other as usize) { @@ -1066,7 +1066,7 @@ impl ChunkLayout<'_, '_> { .c .should_include_part(source_index, &parts[part_index as usize])) { - let is_runtime = source_index == Index::RUNTIME.value(); + let is_runtime = self.c.graph.is_runtime(source_index); // The part is empty in the dev server format, which takes one range per file. if is_namespace_export && !is_runtime @@ -1159,7 +1159,7 @@ fn reached_chunks_in_order( let mut visited = AutoBitSet::init_empty(c.graph.files.len())?; let mut stack: Vec = Vec::new(); - for &root in core::iter::once(&Index::RUNTIME.value()).chain(&roots) { + for root in c.graph.runtimes().chain(roots.iter().copied()) { stack.push(Frame::Enter(root)); while let Some(frame) = stack.pop() { let source_index = match frame { diff --git a/src/bundler/linker_context/generateChunksInParallel.rs b/src/bundler/linker_context/generateChunksInParallel.rs index 9e3251214021..52e253b27905 100644 --- a/src/bundler/linker_context/generateChunksInParallel.rs +++ b/src/bundler/linker_context/generateChunksInParallel.rs @@ -1351,12 +1351,7 @@ pub(crate) fn generate_chunks_in_parallel( break 'brk BakeExtra::default(); } - let mut extra = BakeExtra { - bake_is_runtime: chunk - .files_with_parts_in_chunk - .contains(&Index::RUNTIME.get()), - ..Default::default() - }; + let mut extra = BakeExtra::default(); if output_kind == options::OutputKind::EntryPoint && side == options::Side::Server { diff --git a/src/bundler/linker_context/generateCodeForFileInChunkJS.rs b/src/bundler/linker_context/generateCodeForFileInChunkJS.rs index c0778ac7eae1..e1583084b842 100644 --- a/src/bundler/linker_context/generateCodeForFileInChunkJS.rs +++ b/src/bundler/linker_context/generateCodeForFileInChunkJS.rs @@ -71,7 +71,7 @@ pub fn generate_code_for_file_in_chunk_js<'r, 'src>( // - one part range per file if c.options.output_format == OutputFormat::InternalBakeDev { 'brk: { - if part_range.source_index.is_runtime() { + if c.graph.is_runtime(part_range.source_index.get()) { debug_assert!(c.dev_server.is_none()); break 'brk; // this is from `bun build --format=internal_bake_dev` } @@ -608,7 +608,7 @@ pub fn generate_code_for_file_in_chunk_js<'r, 'src>( E::Call { target: Expr::init( E::Identifier { - ref_: c.cjs_runtime_ref, + ref_: c.runtime_refs(part_range.source_index.get()).cjs, ..Default::default() }, bun_ast::Loc::EMPTY, @@ -875,7 +875,10 @@ pub fn generate_code_for_file_in_chunk_js<'r, 'src>( // "var init_foo = __esm(...);" let value = Expr::init( E::Call { - target: Expr::init_identifier(c.esm_runtime_ref, bun_ast::Loc::EMPTY), + target: Expr::init_identifier( + c.runtime_refs(part_range.source_index.get()).esm, + bun_ast::Loc::EMPTY, + ), args: Vec::move_from_list(esm_args), ..Default::default() }, diff --git a/src/bundler/linker_context/generateCompileResultForJSChunk.rs b/src/bundler/linker_context/generateCompileResultForJSChunk.rs index 5219d91ef861..834315d79b0a 100644 --- a/src/bundler/linker_context/generateCompileResultForJSChunk.rs +++ b/src/bundler/linker_context/generateCompileResultForJSChunk.rs @@ -9,7 +9,7 @@ use crate::analyze_transpiled_module::ModuleInfo; use crate::linker_context_mod::LinkerContext; use crate::options::OutputFormat; use crate::thread_pool::Worker; -use crate::{Chunk, CompileResult, Index, PartRange}; +use crate::{Chunk, CompileResult, PartRange}; use super::generate_code_for_file_in_chunk_js::generate_code_for_file_in_chunk_js; @@ -99,8 +99,11 @@ fn generate_compile_result_for_js_chunk_impl( .expect("Worker.stmt_list set in create()"); stmt_list.reset(); - let runtime_scope: &mut Scope = &mut c.graph.ast.items_module_scope_mut() - [c.graph.files.items_input_file()[Index::RUNTIME.get() as usize].get() as usize]; + let source_index = part_range.source_index.get(); + let runtime_index = c.graph.runtime_index(source_index); + let output_format = c.output_format_for(source_index); + let runtime_scope: &mut Scope = + &mut c.graph.ast.items_module_scope_mut()[runtime_index.get() as usize]; let runtime_members = &runtime_scope.members; let to_common_js_ref = c.graph.symbols.follow( runtime_members @@ -112,7 +115,7 @@ fn generate_compile_result_for_js_chunk_impl( .graph .symbols .follow(runtime_members.get(b"__toESM".as_slice()).unwrap().ref_); - let runtime_require_ref = if c.options.output_format == OutputFormat::Cjs { + let runtime_require_ref = if output_format == OutputFormat::Cjs { None } else { Some( @@ -161,7 +164,7 @@ fn generate_compile_result_for_js_chunk_impl( PrintResult::Result(r) => r.code.len(), _ => 0, }; - if code_len > 0 && !part_range.source_index.is_runtime() { + if code_len > 0 && !c.graph.is_runtime(source_index) { // CONCURRENCY: the map's key set is frozen before parallel codegen; we // only need a shared `&AtomicUsize` to RMW the counter. Using `get` // (not `get_ptr_mut`) avoids materializing an aliased `&mut` to a slot diff --git a/src/bundler/linker_context/mergeSmallChunks.rs b/src/bundler/linker_context/mergeSmallChunks.rs index 86f752290986..d078296e09e1 100644 --- a/src/bundler/linker_context/mergeSmallChunks.rs +++ b/src/bundler/linker_context/mergeSmallChunks.rs @@ -5,7 +5,7 @@ use bun_collections::{ArrayHashMap, AutoBitSet, MapEntry}; use crate::linker_context_mod::debug; use crate::options::{Loader, Target}; -use crate::{EntryPoint, Index, LinkerContext, WrapKind}; +use crate::{EntryPoint, LinkerContext, WrapKind}; bun_core::define_scoped_log!(debug_merge, MergeChunks, hidden); @@ -1020,7 +1020,7 @@ pub(crate) fn merge_small_chunks( let target = ast_targets[source_index as usize]; // Only the runtime helpers a chunk uses are emitted, so its source // size says nothing about the chunk's; count it as free. - let size = if source_index == Index::RUNTIME.value() { + let size = if this.graph.is_runtime(source_index) { 0 } else { sources[source_index as usize].contents().len() as u64 diff --git a/src/bundler/linker_context/postProcessJSChunk.rs b/src/bundler/linker_context/postProcessJSChunk.rs index 2e7da0c1c7fb..2f0494c0500b 100644 --- a/src/bundler/linker_context/postProcessJSChunk.rs +++ b/src/bundler/linker_context/postProcessJSChunk.rs @@ -54,7 +54,10 @@ fn module_preload_registration( { return Ok(code); } - let chunks_ref = c.graph.symbols.follow(c.chunks_runtime_ref); + let chunks_ref = c + .graph + .symbols + .follow(c.runtime_refs(chunk.entry_point.source_index()).chunks); // `reached[i]` gets local index `i`; sites and `seen` use the stable `id()`. let reached = Chunk::reachable_chunks( @@ -133,9 +136,11 @@ pub(crate) fn post_process_js_chunk( let mut cross_chunk_prefix: PrintResult; let mut cross_chunk_suffix: PrintResult; - let runtime_input_file = - c.graph.files.items_input_file()[Index::RUNTIME.value() as usize].get() as usize; - let runtime_scope: &mut Scope = &mut c.graph.ast.items_module_scope_mut()[runtime_input_file]; + let entry_source_index = chunk.entry_point.source_index(); + let runtime_index = c.graph.runtime_index(entry_source_index); + let entry_output_format = c.output_format_for(entry_source_index); + let runtime_scope: &mut Scope = + &mut c.graph.ast.items_module_scope_mut()[runtime_index.value() as usize]; let runtime_members = &mut runtime_scope.members; let to_common_js_ref = c .graph @@ -145,7 +150,7 @@ pub(crate) fn post_process_js_chunk( .graph .symbols .follow(runtime_members.get(&b"__toESM"[..]).unwrap().ref_); - let runtime_require_ref = if c.options.output_format == options::OutputFormat::Cjs { + let runtime_require_ref = if entry_output_format == options::OutputFormat::Cjs { None } else { Some( @@ -491,12 +496,12 @@ pub(crate) fn post_process_js_chunk( // Right after the runtime (which declares `__chunks`) if it is in this chunk, else first. let preload_registration_index = compile_results .iter() - .take_while(|r| r.source_index() == Index::RUNTIME.value()) + .take_while(|r| c.graph.is_runtime(r.source_index())) .count(); if c.options.output_format == options::OutputFormat::InternalBakeDev { for compile_result in compile_results.iter() { let source_index = compile_result.source_index(); - if source_index != Index::RUNTIME.value() { + if !c.graph.is_runtime(source_index) { break; } line_offset.advance(compile_result.code()); @@ -540,7 +545,7 @@ pub(crate) fn post_process_js_chunk( } // Concatenate the generated JavaScript chunks together - let mut prev_filename_comment: IndexInt = 0; + let mut prev_filename_comment: IndexInt = Index::INVALID.value(); let mut compile_results_for_source_map: MultiArrayList = MultiArrayList::default(); @@ -555,7 +560,7 @@ pub(crate) fn post_process_js_chunk( let targets: &[options::Target] = c.parse_graph().ast.items_target(); for (compile_result_index, compile_result) in compile_results.iter().enumerate() { let source_index = compile_result.source_index(); - let is_runtime = source_index == Index::RUNTIME.value(); + let is_runtime = c.graph.is_runtime(source_index); if compile_result_index == preload_registration_index && !preload_registration.is_empty() { newline_before_comment = true; @@ -565,8 +570,10 @@ pub(crate) fn post_process_js_chunk( // TODO: extracated legal comments - // Add a comment with the file path before the file contents + // Add a comment with the file path before the file contents. The + // runtime helpers get none. if show_comments + && !is_runtime && source_index != prev_filename_comment && !compile_result.code().is_empty() { diff --git a/src/bundler/linker_context/renameSymbolsInChunk.rs b/src/bundler/linker_context/renameSymbolsInChunk.rs index 4e21dd4c3083..2101e0c516e8 100644 --- a/src/bundler/linker_context/renameSymbolsInChunk.rs +++ b/src/bundler/linker_context/renameSymbolsInChunk.rs @@ -355,7 +355,9 @@ pub(crate) unsafe fn rename_symbols_in_chunk( // wrapper if the output format supports import statements. We need to // add those symbols to the top-level scope to avoid causing name // collisions. This code special-cases only those symbols. - if c.options.output_format.keep_es6_import_export_syntax() { + if c.output_format_for(source_index) + .keep_es6_import_export_syntax() + { let import_records = all_import_records[source_index as usize].as_slice(); for part in parts.iter() { for stmt in part.stmts.slice() { diff --git a/src/bundler/linker_context/scanImportsAndExports.rs b/src/bundler/linker_context/scanImportsAndExports.rs index ffca995c85b8..4bab2bc9ff8c 100644 --- a/src/bundler/linker_context/scanImportsAndExports.rs +++ b/src/bundler/linker_context/scanImportsAndExports.rs @@ -419,9 +419,10 @@ pub(crate) fn scan_imports_and_exports( // resulting wrapper won't be invoked by other files. An exception is // made for entry point files in CommonJS format (or when in pass-through mode). if kind == ExportsKind::Cjs - && (!col_ref!(entry_point_kinds)[id].is_entry_point() - || output_format == Format::Iife - || output_format == Format::Esm) + && (!col_ref!(entry_point_kinds)[id].is_entry_point() || { + let file_output_format = this.output_format_for(id as u32); + file_output_format == Format::Iife || file_output_format == Format::Esm + }) { col!(flags)[id].wrap = WrapKind::Cjs; } @@ -447,6 +448,8 @@ pub(crate) fn scan_imports_and_exports( export_star_map: HashMap::default(), export_star_records: &*export_star_import_records, output_format, + browser_runtime: this.graph.browser_runtime, + targets: &*ast.target, wrap_stack: Vec::new(), export_star_stack: Vec::new(), } @@ -742,8 +745,6 @@ pub(crate) fn scan_imports_and_exports( let _trace = perf::trace("Bundler.BindImportsToExports"); // const needs_export_symbol_from_runtime: []const bool = this.graph.meta.items().needs_export_symbol_from_runtime; - let mut runtime_export_symbol_ref: Ref = Ref::NONE; - let mut runtime_export_cjs_symbol_ref: Ref = Ref::NONE; let mut ident_scratch: Vec = Vec::new(); let module_preload = this.module_preload(); this.entry_point_part_indices = vec![u32::MAX; col_ref!(import_records_list).len()]; @@ -907,17 +908,11 @@ pub(crate) fn scan_imports_and_exports( // previous step. The previous step can't do this because it's running in // parallel and can't safely mutate the "importsToBind" map of another file. if flag.needs_export_symbol_from_runtime { + let runtime = this.graph.runtime_index(source_index); let export_symbol_ref = if col_ref!(lifted_setter_params)[id].is_valid() { - if !runtime_export_cjs_symbol_ref.is_valid() { - runtime_export_cjs_symbol_ref = this.runtime_function(b"__exportCjs"); - } - runtime_export_cjs_symbol_ref + this.runtime_function(source_index, b"__exportCjs") } else { - if !runtime_export_symbol_ref.is_valid() { - runtime_export_symbol_ref = - this.runtime_function(this.export_runtime_function()); - } - runtime_export_symbol_ref + this.runtime_function(source_index, this.export_runtime_function()) }; debug_assert!(export_symbol_ref.is_valid()); @@ -927,7 +922,7 @@ pub(crate) fn scan_imports_and_exports( bun_ast::NAMESPACE_EXPORT_PART_INDEX, export_symbol_ref, 1, - Index::RUNTIME, + runtime, )?; } @@ -1134,8 +1129,9 @@ pub(crate) fn scan_imports_and_exports( } // This is an external import. Check if it will be a "require()" call. + let file_output_format = this.output_format_for(source_index); if kind == ImportKind::Require - || !output_format.keep_es6_import_export_syntax() + || !file_output_format.keep_es6_import_export_syntax() || kind == ImportKind::Dynamic { // We should use "__require" instead of "require" if we're not @@ -1144,7 +1140,7 @@ pub(crate) fn scan_imports_and_exports( // nor does a split `require()` (`import.meta.require`). if kind != ImportKind::Dynamic && !is_external_dyn - && should_call_runtime_require(output_format) + && should_call_runtime_require(file_output_format) { runtime_require_uses += 1; } @@ -1332,7 +1328,10 @@ pub(crate) fn scan_imports_and_exports( Index::source(rec_source_index.get()), )?; } - } else if output_format.keep_es6_import_export_syntax() { + } else if this + .output_format_for(source_index) + .keep_es6_import_export_syntax() + { col!(import_records_list)[id].as_mut_slice() [*import_record_index as usize] .flags @@ -1349,7 +1348,10 @@ pub(crate) fn scan_imports_and_exports( } let mut happens_at_runtime = rec_source_index.is_invalid() - && (!is_entry_point || !output_format.keep_es6_import_export_syntax()); + && (!is_entry_point + || !this + .output_format_for(source_index) + .keep_es6_import_export_syntax()); if rec_source_index.is_valid() { let other_source_index = rec_source_index.get(); let other_id = other_source_index as usize; @@ -1456,6 +1458,8 @@ struct DependencyWrapper<'a> { entry_point_kinds: &'a [EntryPoint::Kind], export_star_records: &'a [bun_alloc::AstVec], output_format: options::Format, + browser_runtime: Index, + targets: &'a [options::Target], wrap_stack: Vec, export_star_stack: Vec, } @@ -1476,6 +1480,17 @@ enum ExportStarEntry { } impl DependencyWrapper<'_> { + /// See `LinkerContext::output_format_for`. + fn output_format_for(&self, source_index: IndexInt) -> options::Format { + if self.browser_runtime.is_valid() + && self.targets[source_index as usize] == options::Target::Browser + { + Format::Esm + } else { + self.output_format + } + } + /// Marks `source_index` as `EsmWithDynamicFallback` if it transitively /// `export *`s from a file whose exports are not statically analyzable /// (CommonJS, or an unresolved external `export *`), along with every file @@ -1505,7 +1520,10 @@ impl DependencyWrapper<'_> { let rec_source_index = self.import_records[source_index as usize].as_slice()[id as usize].source_index; let found_dynamic = if rec_source_index.is_invalid() { - !kind.is_entry_point() || !self.output_format.keep_es6_import_export_syntax() + !kind.is_entry_point() + || !self + .output_format_for(source_index) + .keep_es6_import_export_syntax() } else if rec_source_index.get() != source_index { matches!( self.enter_export_star(rec_source_index.get()), @@ -1566,8 +1584,10 @@ impl DependencyWrapper<'_> { } flag.did_wrap_dependencies = true; - // Never wrap the runtime file since it always comes first - if source_index == Index::RUNTIME.get() { + // Never wrap the runtime files since they always come first + if source_index == Index::RUNTIME.get() + || (self.browser_runtime.is_valid() && source_index == self.browser_runtime.get()) + { continue; } diff --git a/src/js_parser/p.rs b/src/js_parser/p.rs index bd0f56678462..74a772c04338 100644 --- a/src/js_parser/p.rs +++ b/src/js_parser/p.rs @@ -897,12 +897,13 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_ONLY> { pub(crate) const ALLOW_MACROS: bool = !cfg!(target_family = "wasm"); - /// use this instead of checking p.source.index - /// because when not bundling, p.source.index is `0` + /// Whether this source is a `bun:runtime` module. Do not check + /// `p.source.index`: when not bundling it is `0`, and a server-side build + /// with a browser side has a second runtime at a later index. The bundler + /// clears `allow_runtime` for exactly the runtime sources. #[inline] pub(crate) fn is_source_runtime(&self) -> bool { - // Index 0 is the synthetic runtime chunk. - self.options.bundle && self.source.index.0 == 0 + self.options.bundle && !self.options.features.allow_runtime } /// Extracts a matchable "shape" from a dynamic import argument. diff --git a/src/runtime/bake/production/mod.rs b/src/runtime/bake/production/mod.rs index 673c8ba7e322..175fdcd4dbb1 100644 --- a/src/runtime/bake/production/mod.rs +++ b/src/runtime/bake/production/mod.rs @@ -662,8 +662,6 @@ fn build_with_vm(ctx: Context, cwd: &[u8], pt: &mut PerThread) -> crate::Result< // mkdir -p + open. let root_dir = bun_sys::Dir::cwd().make_open_path(b"dist", Default::default())?; - let mut maybe_runtime_file_index: Option = None; - let mut css_chunks_count: usize = 0; let mut css_chunks_first: usize = 0; @@ -701,17 +699,6 @@ fn build_with_vm(ctx: Context, cwd: &[u8], pt: &mut PerThread) -> crate::Result< } } - // The output file which contains the runtime (Index.runtime, contains - // wrapper functions like `__esm`) is marked as server side, but it is - // also used by client - if file.bake_extra.bake_is_runtime { - debug_assert!( - maybe_runtime_file_index.is_none(), - "Runtime file should only be in one chunk." - ); - maybe_runtime_file_index = Some(u32::try_from(i).expect("int cast")); - } - // TODO: Maybe not do all the disk-writing in 1 thread? let Some(side) = file.side else { continue }; match side { @@ -800,26 +787,6 @@ fn build_with_vm(ctx: Context, cwd: &[u8], pt: &mut PerThread) -> crate::Result< } } } - // Write the runtime file to disk if there are any client chunks. The runtime - // file doesn't exist if no bundled module uses its helpers. - if let Some(runtime_file_index) = maybe_runtime_file_index { - let any_client_chunks = bundled_outputs_list.iter().any(|file| { - file.side == Some(bun_bundler::options::Side::Client) - && file.src_path.text != b"bun-framework-react/client.tsx" - }); - if any_client_chunks { - let runtime_file: &OutputFile = &bundled_outputs_list[runtime_file_index as usize]; - if let Err(err) = runtime_file.write_to_disk(root_dir.fd()) { - bun_core::handle_error_return_trace(err); - Output::err( - err, - "Failed to write {} to output directory", - (bun_core::fmt::quote(&runtime_file.dest_path),), - ); - } - } - } - *pt = PerThread::init( vm_ptr, entry_points, diff --git a/test/bundler/bundler_html_server.test.ts b/test/bundler/bundler_html_server.test.ts index 83a918b1111f..fe559a15fd8b 100644 --- a/test/bundler/bundler_html_server.test.ts +++ b/test/bundler/bundler_html_server.test.ts @@ -1,7 +1,169 @@ -import { describe } from "bun:test"; -import { itBundled } from "./expectBundled"; +import { describe, expect } from "bun:test"; +import { runInNewContext } from "node:vm"; +import { type BundlerTestBundleAPI, itBundled } from "./expectBundled"; + +// A dependency whose published dist ships esbuild's CommonJS interop prelude. In a browser the +// `typeof require` guard falls through to the fallback function. The bundler folds the guard to its +// own `__require`, so the client chunk is only correct with the browser runtime's `__require`. +const esbuildRequirePrelude = /* js */ ` + var __require = ((x) => typeof require < "u" ? require : typeof Proxy < "u" ? new Proxy(x, { get: (a, b) => (typeof require < "u" ? require : a)[b] }) : x)(function (x) { if (typeof require < "u") return require.apply(this, arguments); throw Error('Dynamic require of "' + x + '" is not supported'); }); + export function lazyRequire(name) { return __require(name); } + export const greet = () => "dep loaded"; +`; + +const htmlImportClientFiles = { + "/index.html": /* html */ ``, + "/app.js": /* js */ ` + import { greet, lazyRequire } from "./dep.js"; + globalThis.__lazyRequire = lazyRequire; + using handle = { [Symbol.dispose]() { globalThis.__disposed = true; } }; + globalThis.__state = greet(); + `, + "/dep.js": esbuildRequirePrelude, +}; + +/** The server entry: it keeps the server runtime, whose `__require` is the real `require`. */ +const serverEntry = /* js */ ` + import html from "./index.html"; + export const load = (name) => (typeof require !== "undefined" ? require(name) : null); + console.log(html.index); +`; + +/** The ``, + "/app.js": /* js */ ` + import { greet } from "https://example.com/greet.js"; + const worker = new URL("./worker.js", import.meta.url); + const state = await greet(worker.href); + document.body.dataset.state = state; + `, + }, + onAfterBundle(api) { + const chunk = clientChunkOf(api, "out/server.js"); + expect(chunk).toContain('from "https://example.com/greet.js"'); + expect(chunk).not.toContain("require("); + expect(chunk).not.toContain("__require"); + expect(chunk).toContain("import.meta.url"); + expect(chunk).not.toContain("file://"); + expect(chunk).toContain("await greet("); + expectServerChunkToKeepServerRuntime(api.readFile("out/server.js"), "bun", "cjs"); + }, + }); + + itBundled("compile/html-import-client-chunk-uses-browser-runtime-bytecode", { + compile: true, + bytecode: true, + minifySyntax: true, + files: { + "/entry.ts": /* js */ ` + import html from "./index.html"; + // Bytecode forces the CommonJS format, which has no top-level await. + async function main() { + using server = Bun.serve({ port: 0, routes: { "/": html } }); + const page = await (await fetch(server.url)).text(); + const src = /]*\\ssrc="([^"]+)"/.exec(page)[1]; + const chunk = await (await fetch(new URL(src, server.url))).text(); + console.log("import.meta.require:", chunk.includes("import.meta.require")); + console.log("typeof require guard:", chunk.includes("typeof require")); + } + main(); + `, + ...htmlImportClientFiles, + }, + run: { + stdout: "import.meta.require: false\ntypeof require guard: true", + }, + }); + for (const backend of ["api", "cli"] as const) { itBundled(`compile/${backend}/HTMLServerBasic`, { compile: true, From b47e456bd710220d0376d93fe433ec6f6e0df5b9 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 21 Oct 2026 04:06:31 +0000 Subject: [PATCH 2/8] bundler: add the bake browser runtime after the entry points, keep browser chunks out of IIFE wrappers and bytecode --- src/bundler/LinkerContext.rs | 5 +++- src/bundler/LinkerGraph.rs | 27 ++++++++++++++----- src/bundler/bundle_v2.rs | 9 ++++--- .../linker_context/postProcessJSChunk.rs | 5 ++-- .../linker_context/scanImportsAndExports.rs | 13 +++++---- test/bundler/bundler_html_server.test.ts | 7 +++-- 6 files changed, 43 insertions(+), 23 deletions(-) diff --git a/src/bundler/LinkerContext.rs b/src/bundler/LinkerContext.rs index e1c04d405488..427edd4136ed 100644 --- a/src/bundler/LinkerContext.rs +++ b/src/bundler/LinkerContext.rs @@ -908,7 +908,10 @@ impl<'a> LinkerContext<'a> { } else { crate::options::Loader::Js }; - chunk.content.is_javascript() && loader.is_javascript_like() + chunk.content.is_javascript() + && loader.is_javascript_like() + // Bytecode is for Bun. The browser side of a server build never runs in it. + && self.chunk_side(chunk) == crate::options::Side::Server } /// See [`Self::load`] for why `bundle` is a raw `*mut` (caller passes diff --git a/src/bundler/LinkerGraph.rs b/src/bundler/LinkerGraph.rs index a5c85453d3cf..22db91fc7ea9 100644 --- a/src/bundler/LinkerGraph.rs +++ b/src/bundler/LinkerGraph.rs @@ -525,12 +525,29 @@ pub(crate) fn generate_symbol_import_and_use( Ok(()) } +/// See [`LinkerGraph::is_runtime`]. A free function for callers that hold +/// split-borrowed columns instead of the graph. +#[inline] +pub(crate) fn is_runtime(browser_runtime: Index, source_index: index::Int) -> bool { + source_index == Index::RUNTIME.get() + || (browser_runtime.is_valid() && source_index == browser_runtime.get()) +} + +/// See [`LinkerGraph::runtime_index_for_target`]. +#[inline] +pub(crate) fn runtime_index_for_target(browser_runtime: Index, target: bun_ast::Target) -> Index { + if browser_runtime.is_valid() && target == bun_ast::Target::Browser { + browser_runtime + } else { + Index::RUNTIME + } +} + impl<'a> LinkerGraph<'a> { /// Whether `source_index` is one of the `bun:runtime` modules. #[inline] pub(crate) fn is_runtime(&self, source_index: index::Int) -> bool { - source_index == Index::RUNTIME.get() - || (self.browser_runtime.is_valid() && source_index == self.browser_runtime.get()) + is_runtime(self.browser_runtime, source_index) } /// Every `bun:runtime` module in the graph, the main one first. @@ -556,11 +573,7 @@ impl<'a> LinkerGraph<'a> { /// See [`Self::runtime_index`]. #[inline] pub(crate) fn runtime_index_for_target(&self, target: bun_ast::Target) -> Index { - if self.browser_runtime.is_valid() && target == bun_ast::Target::Browser { - self.browser_runtime - } else { - Index::RUNTIME - } + runtime_index_for_target(self.browser_runtime, target) } /// The symbol for the runtime export `name` that `source_index` links against. diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index fcc8e8136308..fe8983f1bc0d 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -3774,7 +3774,6 @@ pub mod bv2_impl { ) -> Result<(), Error> { self.enqueue_entry_points_common()?; self.reserve_source_indexes_for_bake()?; - self.ensure_browser_runtime()?; let num_entry_points = data.files.count(); self.graph.entry_points.reserve(num_entry_points); @@ -3804,6 +3803,7 @@ pub mod bv2_impl { continue; }; } + self.ensure_browser_runtime()?; Ok(()) } @@ -3858,9 +3858,10 @@ pub mod bv2_impl { } /// Adds the browser runtime once the build has a browser side: when the - /// client transpiler is created for an HTML import, or when a bake - /// production build starts with client entry points. Every browser file - /// is enqueued after this, so `runtime_source_index` never misses it. + /// client transpiler is created for an HTML import, or once a bake + /// production build has enqueued its entry points (they must take the + /// source indices right after the reserved ones). Import records resolve + /// later on the bundle thread, so `runtime_source_index` never misses it. pub(crate) fn ensure_browser_runtime(&mut self) -> Result<(), Error> { if self.has_browser_runtime() && !self.graph.browser_runtime.is_valid() { self.graph.browser_runtime = self.enqueue_runtime(Target::Browser)?; diff --git a/src/bundler/linker_context/postProcessJSChunk.rs b/src/bundler/linker_context/postProcessJSChunk.rs index 2f0494c0500b..523ba7df8548 100644 --- a/src/bundler/linker_context/postProcessJSChunk.rs +++ b/src/bundler/linker_context/postProcessJSChunk.rs @@ -388,7 +388,8 @@ pub(crate) fn post_process_js_chunk( ..Default::default() }; // errdefer j.deinit() — deleted; StringJoiner has Drop - let output_format = c.options.output_format; + // The chunk's own format: the browser side of a server build is ESM. + let output_format = entry_output_format; let mut line_offset: SourceMap::LineColumnOffsetOptional = if c.options.source_maps != options::SourceMapOption::None { @@ -509,7 +510,7 @@ pub(crate) fn post_process_js_chunk( } } - match c.options.output_format { + match output_format { options::OutputFormat::InternalBakeDev => { let start = get_hmr_runtime(if c.options.target.is_server_side() { HmrRuntimeSide::Server diff --git a/src/bundler/linker_context/scanImportsAndExports.rs b/src/bundler/linker_context/scanImportsAndExports.rs index 4bab2bc9ff8c..298d9477812b 100644 --- a/src/bundler/linker_context/scanImportsAndExports.rs +++ b/src/bundler/linker_context/scanImportsAndExports.rs @@ -1482,12 +1482,13 @@ enum ExportStarEntry { impl DependencyWrapper<'_> { /// See `LinkerContext::output_format_for`. fn output_format_for(&self, source_index: IndexInt) -> options::Format { - if self.browser_runtime.is_valid() - && self.targets[source_index as usize] == options::Target::Browser + let target = self.targets[source_index as usize]; + if crate::linker_graph::runtime_index_for_target(self.browser_runtime, target) + == Index::RUNTIME { - Format::Esm - } else { self.output_format + } else { + Format::Esm } } @@ -1585,9 +1586,7 @@ impl DependencyWrapper<'_> { flag.did_wrap_dependencies = true; // Never wrap the runtime files since they always come first - if source_index == Index::RUNTIME.get() - || (self.browser_runtime.is_valid() && source_index == self.browser_runtime.get()) - { + if crate::linker_graph::is_runtime(self.browser_runtime, source_index) { continue; } diff --git a/test/bundler/bundler_html_server.test.ts b/test/bundler/bundler_html_server.test.ts index fe559a15fd8b..0c82caa76116 100644 --- a/test/bundler/bundler_html_server.test.ts +++ b/test/bundler/bundler_html_server.test.ts @@ -59,10 +59,12 @@ function expectClientChunkToRunInBrowser(chunk: string) { } /** The server chunk still links the runtime of its own target and format. */ -function expectServerChunkToKeepServerRuntime(chunk: string, target: "bun" | "node", format: "esm" | "cjs") { +function expectServerChunkToKeepServerRuntime(chunk: string, target: "bun" | "node", format: "esm" | "cjs" | "iife") { expect(chunk).not.toContain("Dynamic require of"); if (format === "cjs") { expect(chunk).toContain("require(name)"); + } else if (format === "iife") { + expect(chunk.startsWith("// @bun\n(() => {")).toBe(true); } else if (target === "bun") { expect(chunk).toContain("__require = import.meta.require"); } else { @@ -74,7 +76,8 @@ describe.concurrent("bundler", () => { // The client chunk of an HTML import is browser code whatever the server target and format are. // https://github.com/oven-sh/bun/issues/45227 for (const target of ["bun", "node"] as const) { - for (const format of ["esm", "cjs"] as const) { + for (const format of ["esm", "cjs", "iife"] as const) { + if (format === "iife" && target === "node") continue; itBundled(`html-import/client-chunk-uses-browser-runtime-${target}-${format}`, { outdir: "out/", target, From 1fd46933021f42ab621cc42619ef5bf3ddd0a53e Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 21 Oct 2026 04:31:43 +0000 Subject: [PATCH 3/8] bundler: shorten the runtime module comments --- src/ast/base.rs | 6 ++---- src/bundler/Graph.rs | 8 ++------ src/bundler/LinkerContext.rs | 27 +++++++++------------------ src/bundler/LinkerGraph.rs | 10 +++------- src/bundler/ParseTask.rs | 4 +--- src/bundler/bundle_v2.rs | 16 +++++----------- src/js_parser/p.rs | 6 ++---- 7 files changed, 24 insertions(+), 53 deletions(-) diff --git a/src/ast/base.rs b/src/ast/base.rs index e710aacceb0b..1ec000fbae74 100644 --- a/src/ast/base.rs +++ b/src/ast/base.rs @@ -44,10 +44,8 @@ impl Index { self.0 } - /// The main `bun:runtime` module. Within the parser use - /// `p.is_source_runtime()` instead: index 0 is also the id for single-file - /// transforms. Within the linker use `LinkerGraph::is_runtime`: a - /// server-side build with a browser side has a second runtime module. + /// The main `bun:runtime` module. In the parser use `p.is_source_runtime()`, + /// in the linker `LinkerGraph::is_runtime` (there can be a second runtime). #[inline] pub const fn is_runtime(self) -> bool { self.0 == Self::RUNTIME.0 diff --git a/src/bundler/Graph.rs b/src/bundler/Graph.rs index 3e6228c033d7..c6b325b0e477 100644 --- a/src/bundler/Graph.rs +++ b/src/bundler/Graph.rs @@ -92,12 +92,8 @@ pub struct Graph<'a> { /// Helps skip a loop. pub(crate) has_any_secondary_paths: bool, - /// The `bun:runtime` module of the browser side of a server-side build - /// (HTML imports, server components). `Index::INVALID` when the build has - /// no browser side. The main runtime at `Index::RUNTIME` is the variant for - /// `transpiler.options.target`; browser code needs the browser variant (the - /// guarded `__require` shim, `__using` without native `Symbol.dispose`, - /// `__preload`). + /// The browser-variant `bun:runtime` of the browser side of a server-side + /// build. `Index::INVALID` when the build has no browser side. pub(crate) browser_runtime: Index, } diff --git a/src/bundler/LinkerContext.rs b/src/bundler/LinkerContext.rs index 427edd4136ed..e2fbebc9528f 100644 --- a/src/bundler/LinkerContext.rs +++ b/src/bundler/LinkerContext.rs @@ -71,11 +71,10 @@ pub(crate) use crate::linker_context::post_process_html_chunk::post_process_html pub(crate) use crate::linker_context::post_process_js_chunk::post_process_js_chunk; pub(crate) use crate::linker_context::rename_symbols_in_chunk::rename_symbols_in_chunk; -/// The runtime exports the linker refers to by name while it generates -/// wrappers and chunk prologues. One set per `bun:runtime` module in the graph. +/// The runtime exports the linker names in wrappers and chunk prologues, one set +/// per `bun:runtime` module. #[derive(Clone, Copy)] pub(crate) struct RuntimeRefs { - /// The source index of the runtime module these refs come from. pub(crate) source_index: Index, /// `__commonJS` pub(crate) cjs: Ref, @@ -83,8 +82,7 @@ pub(crate) struct RuntimeRefs { pub(crate) esm: Ref, /// `__promiseAll` pub(crate) promise_all: Ref, - /// `__preload` / `__chunks`: modulepreload for split browser `import()`s. - /// `Ref::NONE` in the non-browser runtime variants. + /// `__preload` / `__chunks` (modulepreload), browser variant only. pub(crate) preload: Ref, pub(crate) chunks: Ref, } @@ -141,11 +139,9 @@ pub struct LinkerContext<'a> { pub(crate) resolver: Option>>, pub(crate) cycle_detector: Vec, - /// Symbols of the main runtime (`Index::RUNTIME`). Use - /// [`Self::runtime_refs`] to pick the runtime a file links against. + /// The runtime at `Index::RUNTIME`. See [`Self::runtime_refs`]. pub(crate) main_runtime: RuntimeRefs, - /// Symbols of the browser runtime (`LinkerGraph::browser_runtime`), when - /// the build has one. + /// The runtime at `LinkerGraph::browser_runtime`, if any. pub(crate) browser_runtime: RuntimeRefs, /// We may need to refer to the CommonJS "module" symbol for exports @@ -405,8 +401,7 @@ impl<'a> LinkerContext<'a> { && self.main_runtime.chunks.is_valid() } - /// The runtime symbols that the code generated for `source_index` links - /// against. See `LinkerGraph::runtime_index`. + /// The runtime that `source_index` links against (`LinkerGraph::runtime_index`). #[inline] pub(crate) fn runtime_refs(&self, source_index: u32) -> &RuntimeRefs { self.runtime_refs_for_target(self.graph.ast.items_target()[source_index as usize]) @@ -422,9 +417,7 @@ impl<'a> LinkerContext<'a> { } } - /// The output format of the chunk that holds `source_index`. The browser - /// side of a server-side build is always ESM (see - /// `BundleV2::initialize_client_transpiler`), whatever the server format. + /// The output format of `source_index`: the browser side of a server build is ESM. #[inline] pub(crate) fn output_format_for(&self, source_index: u32) -> Format { self.output_format_for_target(self.graph.ast.items_target()[source_index as usize]) @@ -3396,8 +3389,7 @@ fn import_tracker_eq(a: &ImportTracker, b: &ImportTracker) -> bool { } impl<'a> LinkerContext<'a> { - /// Looks up the symbol `Ref` for a named export of the runtime module - /// that `source_index` links against. + /// The `Ref` of runtime export `name` in the runtime `source_index` links against. #[inline] pub(crate) fn runtime_function(&self, source_index: u32, name: &[u8]) -> Ref { self.graph.runtime_function(source_index, name) @@ -3410,8 +3402,7 @@ impl<'a> LinkerContext<'a> { self.graph.top_level_symbol_to_parts(id, r#ref) } - /// Returns the part indices in the runtime module `runtime` that declare - /// the top-level symbol `ref`. + /// The part indices in runtime module `runtime` that declare `ref`. #[inline] pub(crate) fn top_level_symbols_to_parts_for_runtime( &self, diff --git a/src/bundler/LinkerGraph.rs b/src/bundler/LinkerGraph.rs index 22db91fc7ea9..24068b135358 100644 --- a/src/bundler/LinkerGraph.rs +++ b/src/bundler/LinkerGraph.rs @@ -525,8 +525,7 @@ pub(crate) fn generate_symbol_import_and_use( Ok(()) } -/// See [`LinkerGraph::is_runtime`]. A free function for callers that hold -/// split-borrowed columns instead of the graph. +/// See [`LinkerGraph::is_runtime`], for callers that hold split-borrowed columns. #[inline] pub(crate) fn is_runtime(browser_runtime: Index, source_index: index::Int) -> bool { source_index == Index::RUNTIME.get() @@ -560,11 +559,8 @@ impl<'a> LinkerGraph<'a> { ) } - /// The `bun:runtime` module that the code generated for `source_index` - /// links against: the browser runtime for a browser-side file of a - /// server-side build, the main runtime otherwise. Mirrors - /// `BundleV2::runtime_source_index`, which resolved the file's own - /// `bun:wrap` import records. + /// The `bun:runtime` module that `source_index` links against, the same one + /// `BundleV2::runtime_source_index` gave its `bun:wrap` import records. #[inline] pub(crate) fn runtime_index(&self, source_index: index::Int) -> Index { self.runtime_index_for_target(self.ast.items_target()[source_index as usize]) diff --git a/src/bundler/ParseTask.rs b/src/bundler/ParseTask.rs index c83995df1077..ede79bd65d35 100644 --- a/src/bundler/ParseTask.rs +++ b/src/bundler/ParseTask.rs @@ -326,9 +326,7 @@ impl ParseTask { parse_worker::get_runtime_source(target, source_index) } - /// The task parses a `bun:runtime` module. A server-side build with a - /// browser side has two of them (see `BundleV2::runtime_source_index`), so - /// this is not the same as `source_index.is_runtime()`. + /// The task parses a `bun:runtime` module (a build can have two of them). #[inline] pub(crate) fn is_runtime(&self) -> bool { self.path.namespace == RUNTIME_NAMESPACE diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index fe8983f1bc0d..9187d30addd8 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -3814,8 +3814,7 @@ pub mod bv2_impl { Ok(()) } - /// Adds the `bun:runtime` module for `target` as the next source index - /// and maps `bun:wrap` to it in that target's path map. + /// Adds the `bun:runtime` for `target` at the next source index. fn enqueue_runtime(&mut self, target: options::Target) -> Result { let source_index = Index::source(self.graph.input_files.len() as IndexInt); let rt = ParseTask::get_runtime_source(target, source_index); @@ -3847,21 +3846,16 @@ pub mod bv2_impl { Ok(source_index) } - /// Whether the browser side of this server-side build has its own - /// `bun:runtime` module (the browser variant). The dev server format - /// keeps the single one: its module registry provides `bun:wrap`, and - /// the printer knows the runtime as source index 0. + /// Whether the browser side of this server-side build gets its own + /// `bun:runtime`. The dev server format keeps one: it provides `bun:wrap` itself. fn has_browser_runtime(&self) -> bool { self.transpiler.options.target.is_server_side() && self.dev_server.is_none() && self.transpiler.options.output_format != options::Format::InternalBakeDev } - /// Adds the browser runtime once the build has a browser side: when the - /// client transpiler is created for an HTML import, or once a bake - /// production build has enqueued its entry points (they must take the - /// source indices right after the reserved ones). Import records resolve - /// later on the bundle thread, so `runtime_source_index` never misses it. + /// Adds the browser runtime once the build has a browser side, before any + /// browser file's import records resolve. pub(crate) fn ensure_browser_runtime(&mut self) -> Result<(), Error> { if self.has_browser_runtime() && !self.graph.browser_runtime.is_valid() { self.graph.browser_runtime = self.enqueue_runtime(Target::Browser)?; diff --git a/src/js_parser/p.rs b/src/js_parser/p.rs index 74a772c04338..2ad25cc6f293 100644 --- a/src/js_parser/p.rs +++ b/src/js_parser/p.rs @@ -897,10 +897,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_ONLY> { pub(crate) const ALLOW_MACROS: bool = !cfg!(target_family = "wasm"); - /// Whether this source is a `bun:runtime` module. Do not check - /// `p.source.index`: when not bundling it is `0`, and a server-side build - /// with a browser side has a second runtime at a later index. The bundler - /// clears `allow_runtime` for exactly the runtime sources. + /// Whether this source is a `bun:runtime` module. Not `p.source.index == 0`: that + /// is also the id of single-file transforms, and a build can have two runtimes. #[inline] pub(crate) fn is_source_runtime(&self) -> bool { self.options.bundle && !self.options.features.allow_runtime From 783e82e4176804648b3a264130ccbfe890296ccf Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 21 Oct 2026 04:32:50 +0000 Subject: [PATCH 4/8] bundler: shorten three more comments --- src/bundler/bundle_v2.rs | 6 ++---- src/bundler/linker_context/postProcessJSChunk.rs | 3 +-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index 9187d30addd8..809a5454482d 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -1999,8 +1999,7 @@ pub mod bv2_impl { unsafe { Transpiler::for_worker(this_transpiler, arena, this_transpiler.log) }; ct.options.target = Target::Browser; - // The client assets of an HTML import are ES modules whatever the - // server bundle's format is (`--compile --bytecode` forces CommonJS). + // Client assets are ES modules whatever the server format is. ct.options.output_format = options::Format::Esm; // Don't inherit SSR mode from the server target: the SSR pass // drops hook setter bindings, which is invalid for browser code. @@ -7695,8 +7694,7 @@ pub mod bv2_impl { b"__jsonParse", )? .ok_or(Error::ParserError)?; - // The manifest is a server-side module: it links against the - // server runtime's `__jsonParse` and lands in the server chunk. + // The manifest is a server-side module. manifest_ast.target = target; let ast_for_html_entrypoint = JSAst::init(manifest_ast); diff --git a/src/bundler/linker_context/postProcessJSChunk.rs b/src/bundler/linker_context/postProcessJSChunk.rs index 523ba7df8548..04f90c3a77d5 100644 --- a/src/bundler/linker_context/postProcessJSChunk.rs +++ b/src/bundler/linker_context/postProcessJSChunk.rs @@ -571,8 +571,7 @@ pub(crate) fn post_process_js_chunk( // TODO: extracated legal comments - // Add a comment with the file path before the file contents. The - // runtime helpers get none. + // Add a comment with the file path before the file contents (not for the runtime) if show_comments && !is_runtime && source_index != prev_filename_comment From 6a4f46905c50dc61aa19303eebcd02f1dc943f9c Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 21 Oct 2026 05:18:49 +0000 Subject: [PATCH 5/8] bundler: keep the client transpiler's format for the dev server format, tighten the cjs server-chunk assertion --- src/bundler/bundle_v2.rs | 9 ++++++--- test/bundler/bundler_html_server.test.ts | 2 ++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index 809a5454482d..aabc7f8a8203 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -2000,7 +2000,9 @@ pub mod bv2_impl { ct.options.target = Target::Browser; // Client assets are ES modules whatever the server format is. - ct.options.output_format = options::Format::Esm; + if self.has_browser_runtime() { + ct.options.output_format = options::Format::Esm; + } // Don't inherit SSR mode from the server target: the SSR pass // drops hook setter bindings, which is invalid for browser code. if ct.options.react_compiler.is_ssr() { @@ -4832,8 +4834,9 @@ pub mod bv2_impl { }; // SAFETY: see `self_ptr` note above — `transpiler_for_target` needs - // `&mut self` only to pick between two stored `*mut Transpiler`s; it - // never touches `graph.input_files`. + // `&mut self` only to pick between two stored `*mut Transpiler`s. Its + // lazy arm, which appends to `graph.input_files`, is not reached: a + // browser-target file exists only once the client transpiler does. let asset_naming = unsafe { &(*self_ptr) .transpiler_for_target(target) diff --git a/test/bundler/bundler_html_server.test.ts b/test/bundler/bundler_html_server.test.ts index 0c82caa76116..0dee0b7bf380 100644 --- a/test/bundler/bundler_html_server.test.ts +++ b/test/bundler/bundler_html_server.test.ts @@ -62,7 +62,9 @@ function expectClientChunkToRunInBrowser(chunk: string) { function expectServerChunkToKeepServerRuntime(chunk: string, target: "bun" | "node", format: "esm" | "cjs" | "iife") { expect(chunk).not.toContain("Dynamic require of"); if (format === "cjs") { + // The bare global, not the runtime's `__require`. expect(chunk).toContain("require(name)"); + expect(chunk).not.toContain("__require"); } else if (format === "iife") { expect(chunk.startsWith("// @bun\n(() => {")).toBe(true); } else if (target === "bun") { From 7ca5f4ecefe4769ea41110184d0926b64176872c Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 21 Oct 2026 06:03:06 +0000 Subject: [PATCH 6/8] bundler: derive the bake entry point index from the browser runtime's slot --- src/bundler/LinkerContext.rs | 17 +++++++++++++++++ src/bundler/bundle_v2.rs | 2 +- .../linker_context/generateChunksInParallel.rs | 9 +-------- .../linker_context/writeOutputFilesToDisk.rs | 9 +-------- 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/bundler/LinkerContext.rs b/src/bundler/LinkerContext.rs index e2fbebc9528f..1d7d95043763 100644 --- a/src/bundler/LinkerContext.rs +++ b/src/bundler/LinkerContext.rs @@ -894,6 +894,23 @@ impl<'a> LinkerContext<'a> { /// Whether `chunk` is one that bytecode (and, in an executable, module info) is made for. The output file list /// counts those files with this before they are made (`OutputFileList::calculate_output_file_list_capacity`). + /// The position of the entry point `source_index` in the user's entry point + /// list: the sources before it are the runtime, the two bake virtual modules + /// of a server-components build, and the browser runtime when it comes first. + pub(crate) fn bake_entry_point_index(&self, source_index: u32) -> u32 { + let reserved: u32 = if self + .framework + .is_some_and(|fw| fw.server_components.is_some()) + { + 3 + } else { + 1 + }; + let browser_runtime = self.graph.browser_runtime; + let shifted = browser_runtime.is_valid() && browser_runtime.get() < source_index; + source_index - reserved - u32::from(shifted) + } + pub(crate) fn chunk_gets_bytecode(&self, chunk: &Chunk) -> bool { // The CSS chunk of a JavaScript entry point has that entry point's loader. let loader = if chunk.entry_point.is_entry_point() { diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index aabc7f8a8203..1d04d244631d 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -3775,6 +3775,7 @@ pub mod bv2_impl { ) -> Result<(), Error> { self.enqueue_entry_points_common()?; self.reserve_source_indexes_for_bake()?; + self.ensure_browser_runtime()?; let num_entry_points = data.files.count(); self.graph.entry_points.reserve(num_entry_points); @@ -3804,7 +3805,6 @@ pub mod bv2_impl { continue; }; } - self.ensure_browser_runtime()?; Ok(()) } diff --git a/src/bundler/linker_context/generateChunksInParallel.rs b/src/bundler/linker_context/generateChunksInParallel.rs index 52e253b27905..7f833bd6a22a 100644 --- a/src/bundler/linker_context/generateChunksInParallel.rs +++ b/src/bundler/linker_context/generateChunksInParallel.rs @@ -1322,14 +1322,7 @@ pub(crate) fn generate_chunks_in_parallel( module_info_index, side: Some(side), entry_point_index: if output_kind == options::OutputKind::EntryPoint { - Some( - chunk.entry_point.source_index() - - (if let Some(fw) = c.framework { - if fw.server_components.is_some() { 3 } else { 1 } - } else { - 1 - }) as u32, - ) + Some(c.bake_entry_point_index(chunk.entry_point.source_index())) } else { None }, diff --git a/src/bundler/linker_context/writeOutputFilesToDisk.rs b/src/bundler/linker_context/writeOutputFilesToDisk.rs index 82403fa9e140..83e968b524ab 100644 --- a/src/bundler/linker_context/writeOutputFilesToDisk.rs +++ b/src/bundler/linker_context/writeOutputFilesToDisk.rs @@ -549,14 +549,7 @@ pub(crate) fn write_output_files_to_disk( data: OutputFileData::Saved(0), side: Some(c.chunk_side(chunk)), entry_point_index: if output_kind == options::OutputKind::EntryPoint { - // Server-components builds insert 2 extra synthetic sources - // before user entry points, so the source-index offset is 3. - let offset: u32 = if c.framework.is_some_and(|fw| fw.server_components.is_some()) { - 3 - } else { - 1 - }; - Some(chunk.entry_point.source_index() - offset) + Some(c.bake_entry_point_index(chunk.entry_point.source_index())) } else { None }, From 361f8daac30c3d7ca13b62969f5f4ba7a17c06e2 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 21 Oct 2026 06:14:46 +0000 Subject: [PATCH 7/8] bundler: keep the chunk_gets_bytecode doc comment on its function --- src/bundler/LinkerContext.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/bundler/LinkerContext.rs b/src/bundler/LinkerContext.rs index 1d7d95043763..951964f1f0cf 100644 --- a/src/bundler/LinkerContext.rs +++ b/src/bundler/LinkerContext.rs @@ -892,11 +892,8 @@ impl<'a> LinkerContext<'a> { } } - /// Whether `chunk` is one that bytecode (and, in an executable, module info) is made for. The output file list - /// counts those files with this before they are made (`OutputFileList::calculate_output_file_list_capacity`). - /// The position of the entry point `source_index` in the user's entry point - /// list: the sources before it are the runtime, the two bake virtual modules - /// of a server-components build, and the browser runtime when it comes first. + /// The position of entry point `source_index` in the user's entry point list: + /// after the runtime, bake's two virtual modules, and the browser runtime if it came first. pub(crate) fn bake_entry_point_index(&self, source_index: u32) -> u32 { let reserved: u32 = if self .framework @@ -911,6 +908,8 @@ impl<'a> LinkerContext<'a> { source_index - reserved - u32::from(shifted) } + /// Whether `chunk` is one that bytecode (and, in an executable, module info) is made for. The output file list + /// counts those files with this before they are made (`OutputFileList::calculate_output_file_list_capacity`). pub(crate) fn chunk_gets_bytecode(&self, chunk: &Chunk) -> bool { // The CSS chunk of a JavaScript entry point has that entry point's loader. let loader = if chunk.entry_point.is_entry_point() { From d9d16e5f340cf43ac3dcb7a473b3a5ed60f62b70 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 21 Oct 2026 06:15:16 +0000 Subject: [PATCH 8/8] bundler: one-line doc for bake_entry_point_index --- src/bundler/LinkerContext.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/bundler/LinkerContext.rs b/src/bundler/LinkerContext.rs index 951964f1f0cf..0f0fa1ee6268 100644 --- a/src/bundler/LinkerContext.rs +++ b/src/bundler/LinkerContext.rs @@ -892,8 +892,7 @@ impl<'a> LinkerContext<'a> { } } - /// The position of entry point `source_index` in the user's entry point list: - /// after the runtime, bake's two virtual modules, and the browser runtime if it came first. + /// Position of entry point `source_index` in the user's list, after the reserved sources. pub(crate) fn bake_entry_point_index(&self, source_index: u32) -> u32 { let reserved: u32 = if self .framework