diff --git a/src/ast/base.rs b/src/ast/base.rs index bc971d13195f..1ec000fbae74 100644 --- a/src/ast/base.rs +++ b/src/ast/base.rs @@ -44,8 +44,8 @@ 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. 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 b7adc95e436c..c6b325b0e477 100644 --- a/src/bundler/Graph.rs +++ b/src/bundler/Graph.rs @@ -91,6 +91,10 @@ pub struct Graph<'a> { /// /// Helps skip a loop. pub(crate) has_any_secondary_paths: bool, + + /// 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, } #[derive(Default)] @@ -179,6 +183,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..0f0fa1ee6268 100644 --- a/src/bundler/LinkerContext.rs +++ b/src/bundler/LinkerContext.rs @@ -71,6 +71,56 @@ 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 names in wrappers and chunk prologues, one set +/// per `bun:runtime` module. +#[derive(Clone, Copy)] +pub(crate) struct RuntimeRefs { + pub(crate) source_index: Index, + /// `__commonJS` + pub(crate) cjs: Ref, + /// `__esm` + pub(crate) esm: Ref, + /// `__promiseAll` + pub(crate) promise_all: Ref, + /// `__preload` / `__chunks` (modulepreload), browser variant only. + 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 +139,14 @@ 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, + /// The runtime at `Index::RUNTIME`. See [`Self::runtime_refs`]. + pub(crate) main_runtime: RuntimeRefs, + /// 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 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 +206,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 +398,39 @@ 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 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]) + } + + /// 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 `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]) + } + + /// 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 +626,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 +662,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 +797,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 +841,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 +856,7 @@ impl<'a> LinkerContext<'a> { Index::part(1u32).get(), actual_ref, 1, - Index::RUNTIME, + runtime, ) .expect("OOM"); } @@ -834,6 +892,21 @@ impl<'a> LinkerContext<'a> { } } + /// 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 + .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) + } + /// 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 { @@ -843,7 +916,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 @@ -907,7 +983,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 +2342,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 +2448,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 +2538,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 +2601,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 +3054,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 +3080,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 +3404,10 @@ 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. + /// The `Ref` of runtime export `name` in the runtime `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 +3417,14 @@ 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`. + /// The part indices in runtime module `runtime` that declare `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 +3522,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 +3540,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 +3549,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 +3598,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 +3644,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 +3653,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 +3667,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 +3704,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 +3716,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 +4062,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..24068b135358 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_ @@ -517,9 +525,60 @@ pub(crate) fn generate_symbol_import_and_use( Ok(()) } +/// 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() + || (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> { - 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 { + is_runtime(self.browser_runtime, source_index) + } + + /// 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 `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]) + } + + /// See [`Self::runtime_index`]. + #[inline] + pub(crate) fn runtime_index_for_target(&self, target: bun_ast::Target) -> Index { + runtime_index_for_target(self.browser_runtime, target) + } + + /// 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 +651,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..ede79bd65d35 100644 --- a/src/bundler/ParseTask.rs +++ b/src/bundler/ParseTask.rs @@ -319,11 +319,23 @@ 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 build can have two of them). + #[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 +579,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 +622,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 +659,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 +1642,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 +2531,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 +2574,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 +2691,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 +2702,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..1d04d244631d 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -1999,6 +1999,10 @@ pub mod bv2_impl { unsafe { Transpiler::for_worker(this_transpiler, arena, this_transpiler.log) }; ct.options.target = Target::Browser; + // Client assets are ES modules whatever the server format is. + 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() { @@ -2055,7 +2059,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 +2406,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 +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); @@ -3800,19 +3810,24 @@ 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` 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); 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,37 @@ 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 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, 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)?; + } 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 +4534,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)?; @@ -4788,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) @@ -6665,13 +6712,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 +6808,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 +7678,28 @@ 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. + 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 +7861,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..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 }, @@ -1351,12 +1344,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..04f90c3a77d5 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( @@ -383,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 { @@ -491,12 +497,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()); @@ -504,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 @@ -540,7 +546,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 +561,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 +571,9 @@ 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 (not for the runtime) 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..298d9477812b 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,18 @@ enum ExportStarEntry { } impl DependencyWrapper<'_> { + /// See `LinkerContext::output_format_for`. + fn output_format_for(&self, source_index: IndexInt) -> options::Format { + let target = self.targets[source_index as usize]; + if crate::linker_graph::runtime_index_for_target(self.browser_runtime, target) + == Index::RUNTIME + { + self.output_format + } else { + Format::Esm + } + } + /// 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 +1521,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 +1585,8 @@ 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 crate::linker_graph::is_runtime(self.browser_runtime, source_index) { continue; } 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 }, diff --git a/src/js_parser/p.rs b/src/js_parser/p.rs index bd0f56678462..2ad25cc6f293 100644 --- a/src/js_parser/p.rs +++ b/src/js_parser/p.rs @@ -897,12 +897,11 @@ 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. 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 { - // 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..0dee0b7bf380 100644 --- a/test/bundler/bundler_html_server.test.ts +++ b/test/bundler/bundler_html_server.test.ts @@ -1,7 +1,174 @@ -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,