diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index 6001a7b6af5a..9ae9c4a7ff69 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -404,9 +404,11 @@ pub mod bv2_impl { /// TYPE_ONLY moved /// down to bundler (T5); bake (in runtime, T6) constructs values of this type. + #[derive(Clone)] pub enum BuiltInModule { - Import(Box<[u8]>), - Code(Box<[u8]>), + Import(std::borrow::Cow<'static, [u8]>), + /// `Cow::Borrowed` is one of Bun's embedded sources. + Code(std::borrow::Cow<'static, [u8]>), } /// `EntryPointList` flags. @@ -475,7 +477,7 @@ pub mod bv2_impl { } impl Framework { /// Construct the bundler-side TYPE_ONLY view. Called from - /// `bun_runtime::bake::Framework::init_transpiler_with_options`; the + /// `bun_runtime::bake::Framework::as_bundler_view`; the /// runtime owns the canonical `bake.Framework` and projects the /// fields the bundler reads. pub fn new( diff --git a/src/bundler/transpiler.rs b/src/bundler/transpiler.rs index 9d50a5b4966b..a496e8e35ad5 100644 --- a/src/bundler/transpiler.rs +++ b/src/bundler/transpiler.rs @@ -563,7 +563,7 @@ impl<'a> Transpiler<'a> { /// The resolver /// crate carries a FORWARD_DECL subset of `BundleOptions`, so re-project - /// rather than `Clone`. Called after `init_transpiler_with_options` mutates + /// rather than `Clone`. Called after `init_transpiler` mutates /// `self.options` so the resolver sees the same conditions/target/public_path. pub fn sync_resolver_opts(&mut self) { self.resolver.opts = resolver_bundle_options_subset(&self.options); @@ -1025,8 +1025,8 @@ fn resolver_bundle_options_subset( .zip(f.built_in_modules.values().iter()) { let rv = match v { - B::Import(p) => R::Import(p.clone()), - B::Code(c) => R::Code(c.clone()), + B::Import(p) => R::Import(p.as_ref().into()), + B::Code(c) => R::Code(c.as_ref().into()), }; m.put(k, rv).expect("oom"); } diff --git a/src/ptr/lib.rs b/src/ptr/lib.rs index 347389c97770..a7afb528f958 100644 --- a/src/ptr/lib.rs +++ b/src/ptr/lib.rs @@ -432,7 +432,7 @@ pub unsafe fn boxed_slices_as_borrowed(s: &[Box<[T // bare `&'static [u8]` carries no proof, so a refactor can silently feed // it a stack slice. // • Population B (~24) — bytes are owned by a value with a `Drop` that runs -// before process exit (UserOptions arena, FetchTasklet, JSC slice, SSL +// before process exit (FetchTasklet, JSC slice, SSL // session). The widen is unsound the moment the value escapes the holder. // // `Interned` is the type-level proof that a `&'static [u8]` came from diff --git a/src/runtime/bake/DevServer.rs b/src/runtime/bake/DevServer.rs index 3654a5a55fa6..2f7d7b5b6c6e 100644 --- a/src/runtime/bake/DevServer.rs +++ b/src/runtime/bake/DevServer.rs @@ -149,12 +149,6 @@ pub(crate) struct Options<'a> { pub broadcast_console_log_from_browser_to_server: bool, } -// Note: the fields (`arena`, `root`, `vm`, `framework`, -// `bundler_options`, `broadcast_console_log_from_browser_to_server`) are -// required with no sensible zero value, so `Default` is intentionally NOT -// implemented. Callers construct `Options` via struct-literal at the call site -// (see `bake_body.rs::UserOptions::into_dev_server_options`). - // The fields `client_graph`, `server_graph`, `directory_watchers`, and `assets` // all use `@fieldParentPointer` to access DevServer's state. This pattern has // made it easier to group related fields together, but one must remember those @@ -507,8 +501,6 @@ pub(crate) fn init(options: Options) -> JsResult> { ); w!(generation, 0); w!(graph_safety_lock, ThreadLock::init_unlocked()); - w!(framework, options.framework); - w!(bundler_options, options.bundler_options); w!(emit_incremental_visualizer_events, 0); w!(emit_memory_visualizer_events, 0); w!( @@ -518,9 +510,7 @@ pub(crate) fn init(options: Options) -> JsResult> { // `dev.frontend_only = dev.framework.file_system_router_types.len == 0` w!( frontend_only, - (*addr_of_mut!((*p).framework)) - .file_system_router_types - .is_empty() + options.framework.file_system_router_types.is_empty() ); w!(client_graph, IncrementalGraph::default()); w!(server_graph, IncrementalGraph::default()); @@ -630,8 +620,7 @@ pub(crate) fn init(options: Options) -> JsResult> { // // SAFETY: `init_transpiler` writes the slot via `MaybeUninit::write` (see // `bake_body.rs`), so the previous (uninitialized) bytes are never dropped. - // `framework`/`log`/`bundler_options` were written above; reborrowing each - // individually via `addr_of_mut!` is sound because no `&mut DevServer` exists. + // `log` was written above; reborrowing it is sound because no `&mut DevServer` exists. // Note: `Transpiler<'static>` erases the arena lifetime — `options.arena` // is the `UserOptions.arena` which is moved into / outlives the `DevServer` // box. Widen `'a → 'static` here once. @@ -644,9 +633,10 @@ pub(crate) fn init(options: Options) -> JsResult> { // accessed below were each written above and are reborrowed disjointly via // `addr_of_mut!`, so no overlapping `&mut` exists. unsafe { - let framework = &mut *addr_of_mut!((*p).framework); + // Still in `options`, so that an early return drops them. + let framework = &options.framework; let log = &mut *addr_of_mut!((*p).log); - let bundler_options = &mut *addr_of_mut!((*p).bundler_options); + let bundler_options = &options.bundler_options; match framework.init_transpiler( arena, @@ -699,6 +689,8 @@ pub(crate) fn init(options: Options) -> JsResult> { } w!(bundler_framework_views, bundler_framework_views); + w!(framework, options.framework); + w!(bundler_options, options.bundler_options); } // ── every field is now written ─────────────────────────────────────────── @@ -753,7 +745,6 @@ pub(crate) fn init(options: Options) -> JsResult> { unsafe { &mut (*(*dev_ptr).server_transpiler.as_mut_ptr()).resolver }, // SAFETY: see above; `client_transpiler` was initialized and is disjoint from `framework`. unsafe { &mut (*(*dev_ptr).client_transpiler.as_mut_ptr()).resolver }, - options.arena, ) .is_err() { diff --git a/src/runtime/bake/bake_body.rs b/src/runtime/bake/bake_body.rs index 7ab1edbfbfb4..e4c31304889b 100644 --- a/src/runtime/bake/bake_body.rs +++ b/src/runtime/bake/bake_body.rs @@ -4,29 +4,28 @@ //! role as a framework, Bake is tool for frameworks to build on top of. #![allow(unexpected_cfgs)] // `bun_codegen_embed` is set via RUSTFLAGS (scripts/build/rust.ts) for release/CI builds. -use bun_alloc::ArenaVecExt as _; use core::ptr::NonNull; +use std::borrow::Cow; -use bun_alloc::Arena; // = bumpalo::Bump -use bun_collections::ArrayHashMap; +use bun_alloc::Arena; +use bun_collections::StringArrayHashMap; use bun_core::Output; -use bun_core::Utf8Bytes; -use bun_core::{ZStr, strings}; -use bun_jsc::{JSGlobalObject, JSValue, JsError, JsResult}; +use bun_core::{Utf8Bytes, ZBox, ZStr, strings}; +use bun_jsc::{JSGlobalObject, JSValue, JsResult}; use bun_options_types::schema as bun_schema; -use bun_paths::{self as paths, PathBuffer}; +use bun_paths::PathBuffer; -// `jsc.API.JSBundler.Plugin` — opaque FFI handle for the C++ JSBundlerPlugin. -// Re-exported from `crate::api::js_bundler` so `SplitBundlerOptions.plugin` -// shares the same type the bundler pipeline uses. -pub(crate) use crate::api::js_bundler::Plugin; +use crate::api::js_bundler::Plugin; use crate::api::js_bundler::js_bundler::PluginJscExt as _; // Note: parent `mod.rs` already declares `dev_server` / `framework_router` // as sibling modules of this file; pull them in instead of re-declaring (which // would duplicate the module tree and fail on `framework_router` having no // matching filename). -use super::{dev_server, framework_router}; +use super::{ + BuildConfigSubset, BuiltInModule, FileSystemRouterType, Framework, ReactFastRefresh, + ServerComponents, SplitBundlerOptions, dev_server, framework_router, +}; // Note: `pub use dev_server as DevServer` / `framework_router as // FrameworkRouter` are already provided by the parent `mod.rs` (lines 349/369); @@ -34,61 +33,21 @@ use super::{dev_server, framework_router}; use bun_bundler_jsc::source_map_mode_jsc::source_map_mode_from_js; -/// Convert a `crate::Error` into a thrown JS exception in a `JsResult` -/// context. -#[inline] -fn throw_core_error(global: &JSGlobalObject, e: crate::Error, ctx: &'static str) -> JsError { - global.throw_error(e, ctx) -} - -/// Erase the `'bump` lifetime of an arena-backed slice. Arena-erasure -/// convention (see file-level TODO(lifetime)): `UserOptions.arena` outlives every -/// borrower, so the bytes are valid for the program-relevant lifetime. -#[inline(always)] -pub(crate) fn arena_erase(r: &T) -> &'static T { - // SAFETY: arena-backed; UserOptions owns the bump and is dropped last. - // PORTING.md sanctions this only inside the bake `from_js` self-referential - // pattern — do NOT generalize. - unsafe { bun_ptr::detach_ref(r) } -} - -/// Copy `bytes` plus a trailing NUL into the bump arena. -/// Returns `&'static ZStr` per the file-level `'static` convention -/// (arena-backed; lifetime erased — see TODO(lifetime) at top of file). -pub(crate) fn arena_dupe_z(arena: &Arena, bytes: &[u8]) -> &'static ZStr { - let buf: &mut [u8] = arena.alloc_slice_fill_default(bytes.len() + 1); - buf[..bytes.len()].copy_from_slice(bytes); - buf[bytes.len()] = 0; - // SAFETY: buf is NUL-terminated; arena outlives all borrowers per the - // self-referential UserOptions pattern. Not `from_buf`: the `'static` - // return type intentionally erases the arena lifetime; threading a real - // `'bump` would replace this with `from_buf`. - unsafe { ZStr::from_raw(buf.as_ptr(), bytes.len()) } -} - /// export default { app: ... }; const API_NAME: &str = "app"; -// TODO(lifetime): many `&'static [u8]` fields below are actually backed -// by `UserOptions.arena` (bumpalo::Bump) or `UserOptions.allocations` -// (StringRefList). `&'static` is used to avoid struct lifetime params per -// PORTING.md; could thread `'bump` or introduce `ArenaStr`. - /// Rust version of the TS definition 'Bake.Options' in 'bake.d.ts' pub(crate) struct UserOptions { - /// This arena contains some miscellaneous allocations at startup + /// Backs the transpilers set up from these options. pub(crate) arena: Arena, - pub(crate) _allocations: StringRefList, - pub(crate) root: &'static ZStr, // TODO(lifetime): arena-owned, self-referential with .arena + pub(crate) root: ZBox, pub(crate) framework: Framework, pub(crate) bundler_options: SplitBundlerOptions, } impl Drop for UserOptions { fn drop(&mut self) { - // arena: dropped by Bump's Drop - // allocations: dropped by StringRefList's Drop if let Some(p) = self.bundler_options.plugin { // `p` is the FFI handle returned by `Plugin::create` in // `parse_plugin_array`; `PluginJscExt::destroy` is its paired @@ -103,10 +62,6 @@ impl UserOptions { /// Currently, this function must run at the top of the event loop. pub(crate) fn from_js(config: JSValue, global: &JSGlobalObject) -> JsResult { let arena = Arena::new(); - // errdefer arena.deinit() — handled by Drop - - let mut allocations = StringRefList::EMPTY; - // errdefer allocations.free() — handled by Drop let mut bundler_options = SplitBundlerOptions::default(); if !config.is_object() { @@ -117,7 +72,7 @@ impl UserOptions { if strings::eql(utf8_string.slice(), b"react") { let root = match bun_sys::getcwd_alloc() { - Ok(z) => arena_dupe_z(&arena, z.as_bytes()), + Ok(z) => z, Err(e) => { return Err(global.throw_error( e.to_zig_err(), @@ -126,15 +81,10 @@ impl UserOptions { } }; - let framework = Framework::react(&arena) - .map_err(|e| throw_core_error(global, e, "Framework::react"))?; - return Ok(UserOptions { - // TODO(lifetime): self-referential — `root`/`framework` borrow `arena` root, - framework, + framework: Framework::react(), bundler_options, - _allocations: allocations, arena, }); } @@ -167,16 +117,14 @@ impl UserOptions { } }, global, - &mut allocations, &mut bundler_options, - &arena, )?; - let root: &[u8] = if let Some(slice) = config.get_optional_slice(global, "root")? { - allocations.track(slice) + let root = if let Some(slice) = config.get_optional_slice(global, "root")? { + ZBox::from_bytes(slice.slice()) } else { match bun_sys::getcwd_alloc() { - Ok(z) => arena_dupe_z(&arena, z.as_bytes()).as_bytes(), + Ok(z) => z, Err(e) => { return Err(global .throw_error(e.to_zig_err(), "while querying current working directory")); @@ -188,65 +136,16 @@ impl UserOptions { bundler_options.parse_plugin_array(plugin_array, global)?; } - let root_z = arena_dupe_z(&arena, root); - Ok(UserOptions { - root: root_z, + root, framework, bundler_options, - _allocations: allocations, arena, }) } } -/// Each string stores its allocator since some may hold reference counts to JSC -#[derive(Default)] -pub(crate) struct StringRefList { - pub(crate) strings: Vec>, -} - -impl StringRefList { - pub(crate) const EMPTY: StringRefList = StringRefList { - strings: Vec::new(), - }; - - // Note: returned slice borrows JSC-owned storage kept alive by the - // `Utf8Bytes` now stored in `self.strings`; it is valid only for as - // long as `self` is. Callers that store the result in `Framework` / - // `FileSystemRouterType` / `ServerComponents` fields must thread a `'bump` - // lifetime (or switch those fields to `Box<[u8]>` / `ArenaStr`) — see the - // file-level TODO(lifetime) above. Do NOT paper over this with a `'static` - // transmute (forbidden per PORTING.md §Forbidden — lifetime extension). - pub(crate) fn track(&mut self, str: Utf8Bytes<'static>) -> &'static [u8] { - self.strings.push(str); - let slice = self.strings.last().unwrap().slice(); - // SAFETY: (`Interned::assume` — Population B, holder-backed) the - // `Utf8Bytes` is now owned by `self.strings` and lives exactly as - // long as the `StringRefList`, which is owned by `UserOptions` and - // dropped only when bake teardown runs (`UserOptions::deinit`). The - // returned slice is stored only in `Framework` / `FileSystemRouterType` - // / `ServerComponents` fields that are themselves owned by the same - // `UserOptions`, so no read outlives the holder. NOT process-lifetime - // — a real `'bump` lifetime should eventually be threaded here (see - // file-level TODO(lifetime)); `assume` makes the lie grep-able until then. - unsafe { bun_ptr::Interned::assume(slice) }.as_bytes() - } -} - -#[derive(Default)] -pub(crate) struct SplitBundlerOptions { - pub plugin: Option>, - pub client: BuildConfigSubset, - pub server: BuildConfigSubset, - pub ssr: BuildConfigSubset, -} - impl SplitBundlerOptions { - // Note: was `pub const EMPTY` — `ArrayHashMap::new()` (inside - // `BuildConfigSubset`) is not `const fn`, so this is now a fn-backed - // default. Callers updated to `SplitBundlerOptions::default()`. - fn parse_plugin_array( &mut self, plugin_array: JSValue, @@ -329,20 +228,6 @@ impl SplitBundlerOptions { } } -pub(crate) struct BuildConfigSubset { - pub ignore_dce_annotations: Option, - pub conditions: ArrayHashMap<&'static [u8], ()>, - pub drop: ArrayHashMap<&'static [u8], ()>, - pub env: bun_schema::api::DotEnvBehavior, - pub env_prefix: Option<&'static [u8]>, - pub define: bun_schema::api::StringMap, - pub source_map: bun_schema::api::SourceMapMode, - - pub minify_syntax: Option, - pub minify_identifiers: Option, - pub minify_whitespace: Option, -} - impl BuildConfigSubset { pub(crate) fn from_js( global: &JSGlobalObject, @@ -393,116 +278,57 @@ impl BuildConfigSubset { } } -impl Default for BuildConfigSubset { - fn default() -> Self { - // Note: was `pub const DEFAULT` — `ArrayHashMap::new()` is not - // `const fn`, so this lives behind `Default` instead. - BuildConfigSubset { - ignore_dce_annotations: None, - conditions: ArrayHashMap::new(), - drop: ArrayHashMap::new(), - env: bun_schema::api::DotEnvBehavior::_none, - env_prefix: None, - define: bun_schema::api::StringMap::EMPTY, - source_map: bun_schema::api::SourceMapMode::External, - - minify_syntax: None, - minify_identifiers: None, - minify_whitespace: None, - } - } -} - -/// A "Framework" in our eyes is simply set of bundler options that a framework -/// author would set in order to integrate the framework with the application. -/// Since many fields have default values which may point to static memory, this -/// structure is always arena-allocated, usually owned by the arena in `UserOptions` -/// -/// Full documentation on these fields is located in the TypeScript definitions. -pub(crate) struct Framework { - pub is_built_in_react: bool, - /// `resolve()` rewrites this in place. Stored as an owned `Vec` so - /// `#[derive(Clone)]` deep-copies (a shared `&[T]` would alias and make - /// `resolve()`'s mutation UB). - pub file_system_router_types: Vec, - // static_routers: &'static [&'static [u8]], - pub server_components: Option, - pub react_fast_refresh: Option, - pub built_in_modules: ArrayHashMap<&'static [u8], BuiltInModule>, -} - -impl Default for Framework { - fn default() -> Self { - Self { - is_built_in_react: false, - file_system_router_types: Vec::new(), - server_components: None, - react_fast_refresh: None, - built_in_modules: ArrayHashMap::new(), - } - } -} - impl Framework { /// Bun provides built-in support for using React as a framework. /// Depends on externally provided React /// /// $ bun i react@experimental react-dom@experimental react-refresh@experimental react-server-dom-bun - pub(crate) fn react(arena: &Arena) -> crate::Result { + pub(crate) fn react() -> Framework { // Cannot use .import because resolution must happen from the user's POV - let built_in_values: &[BuiltInModule] = &[ - // Browser-side source: compressed in release builds. - BuiltInModule::Code(bun_zstd::embed_compressed!( - src "runtime/bake/bun-framework-react/client.tsx" - )), - BuiltInModule::Code( + let built_in: [(&[u8], &'static [u8]); 3] = [ + ( + b"bun-framework-react/client.tsx", + // Browser-side source: compressed in release builds. + bun_zstd::embed_compressed!(src "runtime/bake/bun-framework-react/client.tsx"), + ), + ( + b"bun-framework-react/server.tsx", bun_core::runtime_embed_file!(Src, "runtime/bake/bun-framework-react/server.tsx") .as_bytes(), ), - BuiltInModule::Code( + ( + b"bun-framework-react/ssr.tsx", bun_core::runtime_embed_file!(Src, "runtime/bake/bun-framework-react/ssr.tsx") .as_bytes(), ), ]; + let mut built_in_modules = StringArrayHashMap::new(); + for (path, code) in built_in { + bun_core::handle_oom( + built_in_modules.put(path, BuiltInModule::Code(Cow::Borrowed(code))), + ); + } - Ok(Framework { + Framework { is_built_in_react: true, - server_components: Some(ServerComponents { - separate_ssr_graph: true, - server_runtime_import: b"react-server-dom-bun/server", - ..ServerComponents::default() - }), + server_components: Some(ServerComponents::new( + true, + Utf8Bytes::Borrowed(b"react-server-dom-bun/server"), + )), react_fast_refresh: Some(ReactFastRefresh::default()), file_system_router_types: vec![FileSystemRouterType { - root: b"pages", - prefix: b"/", - entry_client: Some(b"bun-framework-react/client.tsx"), - entry_server: b"bun-framework-react/server.tsx", + root: Utf8Bytes::Borrowed(b"pages"), + prefix: Utf8Bytes::Borrowed(b"/"), + entry_client: Some(Utf8Bytes::Borrowed(b"bun-framework-react/client.tsx")), + entry_server: Utf8Bytes::Borrowed(b"bun-framework-react/server.tsx"), ignore_underscores: true, - ignore_dirs: &[b"node_modules", b".git"], - extensions: &[b".tsx", b".jsx"], + ignore_dirs: literals(&[b"node_modules", b".git"]), + extensions: literals(&[b".tsx", b".jsx"]), style: framework_router::Style::NextjsPages, allow_layouts: true, }], - // .static_routers = arena.alloc_slice_copy(&[b"public"]), - built_in_modules: { - // Note: was `ArrayHashMap::from_entries(arena, keys, vals)`; - // that constructor doesn't exist on the heap-backed - // `ArrayHashMap` — build it imperatively. `bun.handleOom`. - let keys: [&'static [u8]; 3] = [ - b"bun-framework-react/client.tsx", - b"bun-framework-react/server.tsx", - b"bun-framework-react/ssr.tsx", - ]; - let mut m: ArrayHashMap<&'static [u8], BuiltInModule> = ArrayHashMap::new(); - bun_core::handle_oom(m.ensure_total_capacity(keys.len())); - for (k, v) in keys.iter().zip(built_in_values.iter()) { - m.put_assume_capacity(*k, *v); - } - let _ = arena; // arena param retained for API parity - m - }, - }) + built_in_modules, + } } /// Default that requires no packages or configuration. @@ -511,16 +337,14 @@ impl Framework { /// react-refresh so that it still works. /// - If any file system router types are provided, configure using /// the above react configuration. - /// The provided allocator is not stored. pub(crate) fn auto( - arena: &Arena, resolver: &mut bun_resolver::Resolver, file_system_router_types: Vec, - ) -> crate::Result { - let mut fw: Framework = Framework::none(); + ) -> Framework { + let mut fw = Framework::default(); if !file_system_router_types.is_empty() { - fw = Self::react(arena)?; + fw = Self::react(); fw.file_system_router_types = file_system_router_types; } @@ -528,172 +352,23 @@ impl Framework { fw.react_fast_refresh = Some(ReactFastRefresh { import_source: rfr }); } else if resolve_or_null(resolver, b"react").is_some() { fw.react_fast_refresh = Some(ReactFastRefresh { - import_source: b"react-refresh/runtime/index.js", + import_source: Utf8Bytes::Borrowed(b"react-refresh/runtime/index.js"), }); - let react_refresh_code = BuiltInModule::Code( - bun_zstd::embed_compressed!(codegen "node-fallbacks/react-refresh.js"), - ); - let _ = arena; - fw.built_in_modules.put( - b"react-refresh/runtime/index.js" as &[u8], - react_refresh_code, - )?; - } - - Ok(fw) - } - - /// Unopinionated default. Note: was `pub const NONE` — - /// `ArrayHashMap::new()` is not `const fn`. - pub(crate) fn none() -> Framework { - Framework { - is_built_in_react: false, - file_system_router_types: Vec::new(), - server_components: None, - react_fast_refresh: None, - built_in_modules: ArrayHashMap::new(), - } - } - - /// `Framework.clone()` — manual because `ArrayHashMap` exposes a - /// fallible inherent `clone()` rather than `impl Clone`. - pub(crate) fn clone(&self) -> Framework { - Framework { - is_built_in_react: self.is_built_in_react, - file_system_router_types: self.file_system_router_types.clone(), - server_components: self.server_components, - react_fast_refresh: self.react_fast_refresh, - built_in_modules: bun_core::handle_oom(self.built_in_modules.clone()), - } - } - - pub(crate) fn add_react_install_command_note(log: &mut bun_ast::Log) -> crate::Result<()> { - let clone_line_text = log.clone_line_text; - log.add_msg(bun_ast::Msg { - kind: bun_ast::Kind::Note, - data: bun_ast::range_data( - None, - bun_ast::Range::NONE, - // `range_data` takes `impl Into>`; - // `concat!` yields `&'static str` — go via `.as_bytes()`. - concat!( - "Install the built in react integration with \"", - "bun i react@experimental react-dom@experimental react-server-dom-bun react-refresh@experimental", - "\"" - ) - .as_bytes(), - ) - .clone_line_text(clone_line_text), - ..Default::default() - }); - Ok(()) - } - - /// Given a Framework configuration, this returns another one with all paths resolved. - /// New memory allocated into provided arena. - /// - /// All resolution errors will happen before returning error.ModuleNotFound - /// Errors written into `r.log` - pub(crate) fn resolve( - &self, - server: &mut bun_resolver::Resolver, - client: &mut bun_resolver::Resolver, - arena: &Arena, - ) -> crate::Result { - let mut clone = self.clone(); - let mut had_errors: bool = false; - - if let Some(react_fast_refresh) = &mut clone.react_fast_refresh { - self.resolve_helper( - client, - &mut react_fast_refresh.import_source, - &mut had_errors, - b"react refresh runtime", - ); - } - - if let Some(sc) = &mut clone.server_components { - self.resolve_helper( - server, - &mut sc.server_runtime_import, - &mut had_errors, - b"server components runtime", - ); - // self.resolve_helper(client, &mut sc.client_runtime_import, &mut had_errors); - } - - for fsr in clone.file_system_router_types.iter_mut() { - let top_level_dir = bun_resolver::fs::FileSystem::get().top_level_dir; - fsr.root = arena_erase(arena.alloc_slice_copy(paths::resolve_path::join_abs::< - paths::platform::Auto, - >(top_level_dir, fsr.root))); - if let Some(entry_client) = &mut fsr.entry_client { - self.resolve_helper( - client, - entry_client, - &mut had_errors, - b"client side entrypoint", - ); - } - self.resolve_helper( - client, - &mut fsr.entry_server, - &mut had_errors, - b"server side entrypoint", - ); - } - - if had_errors { - return Err(crate::Error::ModuleNotFound); - } - - Ok(clone) - } - - #[inline] - fn resolve_helper( - &self, - r: &mut bun_resolver::Resolver, - path: &mut &'static [u8], - had_errors: &mut bool, - desc: &[u8], - ) { - if let Some(module) = self.built_in_modules.get(path) { - match module { - BuiltInModule::Import(p) => *path = p, - BuiltInModule::Code(_) => {} - } - return; + bun_core::handle_oom(fw.built_in_modules.put( + b"react-refresh/runtime/index.js", + BuiltInModule::Code(Cow::Borrowed( + bun_zstd::embed_compressed!(codegen "node-fallbacks/react-refresh.js"), + )), + )); } - let top_level_dir = bun_resolver::fs::FileSystem::get().top_level_dir; - let mut result = match r.resolve(top_level_dir, *path, bun_ast::ImportKind::Stmt) { - Ok(res) => res, - Err(err) => { - Output::err( - err, - "Failed to resolve '{}' for framework ({})", - (bstr::BStr::new(path), bstr::BStr::new(desc)), - ); - *had_errors = true; - return; - } - }; - // `resolver::Result::path().text` is `&'static [u8]` already (resolver's - // `Path` alias is `bun_paths::fs::Path<'static>`, populated from the - // `FilenameStore` singleton). No widen needed; the previous - // `arena_erase` here laundered an already-`'static` slice and falsely - // implied arena ownership. See `bun_ptr::Interned` for the type that - // `Path::text` should eventually become. - *path = result.path().unwrap().text; + fw } fn from_js( opts: JSValue, global: &JSGlobalObject, - refs: &mut StringRefList, bundler_options: &mut SplitBundlerOptions, - arena: &Arena, ) -> JsResult { if opts.is_string() { let str = opts.to_bun_string(global)?; @@ -703,13 +378,11 @@ impl Framework { bun_core::warn!( "deprecation notice: 'react-server-components' will be renamed to 'react'" ); - return Framework::react(arena) - .map_err(|e| throw_core_error(global, e, "Framework::react")); + return Ok(Framework::react()); } if str.eq_ascii(b"react") { - return Framework::react(arena) - .map_err(|e| throw_core_error(global, e, "Framework::react")); + return Ok(Framework::react()); } } @@ -758,7 +431,7 @@ impl Framework { let str = prop.to_bun_string(global)?; Some(ReactFastRefresh { - import_source: refs.track(str.into_utf8()), + import_source: str.into_utf8(), }) }; let server_components: Option = 'sc: { @@ -775,54 +448,49 @@ impl Framework { ))); } - Some(ServerComponents { - separate_ssr_graph: 'brk: { - // Intentionally not using a truthiness check - let prop = match sc.get_optional::(global, "separateSSRGraph")? { - Some(p) => p, - None => { - return Err(global.throw_invalid_arguments(format_args!( - "Missing 'framework.serverComponents.separateSSRGraph'" - ))); - } - }; - if prop == JSValue::TRUE { - break 'brk true; - } - if prop == JSValue::FALSE { - break 'brk false; + let separate_ssr_graph = 'brk: { + // Intentionally not using a truthiness check + let prop = match sc.get_optional::(global, "separateSSRGraph")? { + Some(p) => p, + None => { + return Err(global.throw_invalid_arguments(format_args!( + "Missing 'framework.serverComponents.separateSSRGraph'" + ))); } - return Err(global.throw_invalid_arguments(format_args!( - "'framework.serverComponents.separateSSRGraph' must be a boolean" - ))); - }, - server_runtime_import: refs.track( - match sc.get_optional_slice(global, "serverRuntimeImportSource")? { - Some(s) => s, - None => { - return Err(global.throw_invalid_arguments(format_args!( - "Missing 'framework.serverComponents.serverRuntimeImportSource'" - ))); - } - }, - ), - server_register_client_reference: if let Some(slice) = - sc.get_optional_slice(global, "serverRegisterClientReferenceExport")? - { - refs.track(slice) - } else { - b"registerClientReference" - }, - ..ServerComponents::default() - }) + }; + if prop == JSValue::TRUE { + break 'brk true; + } + if prop == JSValue::FALSE { + break 'brk false; + } + return Err(global.throw_invalid_arguments(format_args!( + "'framework.serverComponents.separateSSRGraph' must be a boolean" + ))); + }; + let Some(server_runtime_import) = + sc.get_optional_slice(global, b"serverRuntimeImportSource")? + else { + return Err(global.throw_invalid_arguments(format_args!( + "Missing 'framework.serverComponents.serverRuntimeImportSource'" + ))); + }; + let mut server_components = + ServerComponents::new(separate_ssr_graph, server_runtime_import); + if let Some(name) = + sc.get_optional_slice(global, b"serverRegisterClientReferenceExport")? + { + server_components.server_register_client_reference = name; + } + Some(server_components) }; - let built_in_modules: ArrayHashMap<&'static [u8], BuiltInModule> = 'built_in_modules: { + let built_in_modules: StringArrayHashMap = 'built_in_modules: { let Some(array) = opts.get_array(global, "builtInModules")? else { - break 'built_in_modules ArrayHashMap::new(); + break 'built_in_modules StringArrayHashMap::new(); }; let len = array.get_length(global)?; - let mut files: ArrayHashMap<&'static [u8], BuiltInModule> = ArrayHashMap::new(); + let mut files: StringArrayHashMap = StringArrayHashMap::new(); bun_core::handle_oom(files.ensure_total_capacity(len as usize)); let mut it = array.array_iterator(global)?; @@ -835,7 +503,7 @@ impl Framework { ))); } - let path = match get_optional_string(file, global, b"import", refs)? { + let path = match file.get_optional_slice(global, b"import")? { Some(p) => p, None => { return Err(global.throw_invalid_arguments(format_args!( @@ -846,10 +514,10 @@ impl Framework { }; let value: BuiltInModule = - if let Some(str) = get_optional_string(file, global, b"path", refs)? { - BuiltInModule::Import(str) - } else if let Some(str) = get_optional_string(file, global, b"code", refs)? { - BuiltInModule::Code(str) + if let Some(str) = file.get_optional_slice(global, b"path")? { + BuiltInModule::Import(Cow::Owned(str.into_vec())) + } else if let Some(str) = file.get_optional_slice(global, b"code")? { + BuiltInModule::Code(Cow::Owned(str.into_vec())) } else { return Err(global.throw_invalid_arguments(format_args!( "'builtInModules[{}]' needs either 'path' or 'code'", @@ -857,7 +525,7 @@ impl Framework { ))); }; - files.put_assume_capacity(path, value); + files.put_assume_capacity(path.slice(), value); i += 1; } @@ -878,13 +546,12 @@ impl Framework { "Framework can only define up to 256 file-system router types" ))); } - // Note: reshaped alloc+index → Vec::push (owned; deep-cloned with Framework) let mut file_system_router_types = Vec::with_capacity(len as usize); let mut it = array.array_iterator(global)?; let mut i: usize = 0; while let Some(fsr_opts) = it.next()? { - let root = match get_optional_string(fsr_opts, global, b"root", refs)? { + let root = match fsr_opts.get_optional_slice(global, b"root")? { Some(r) => r, None => { return Err(global.throw_invalid_arguments(format_args!( @@ -894,7 +561,7 @@ impl Framework { } }; let server_entry_point = - match get_optional_string(fsr_opts, global, b"serverEntryPoint", refs)? { + match fsr_opts.get_optional_slice(global, b"serverEntryPoint")? { Some(s) => s, None => { return Err(global.throw_invalid_arguments(format_args!( @@ -904,9 +571,10 @@ impl Framework { } }; let client_entry_point = - get_optional_string(fsr_opts, global, b"clientEntryPoint", refs)?; - let prefix = - get_optional_string(fsr_opts, global, b"prefix", refs)?.unwrap_or(b"/"); + fsr_opts.get_optional_slice(global, b"clientEntryPoint")?; + let prefix = fsr_opts + .get_optional_slice(global, b"prefix")? + .unwrap_or(Utf8Bytes::Borrowed(b"/")); let ignore_underscores = fsr_opts .get_boolean_strict(global, "ignoreUnderscores")? .unwrap_or(false); @@ -928,25 +596,20 @@ impl Framework { )?; // errdefer style.deinit() — handled by Style's Drop - let extensions: &'static [&'static [u8]] = if let Some(exts_js) = - fsr_opts.get(global, "extensions")? - { + let extensions = if let Some(exts_js) = fsr_opts.get(global, "extensions")? { 'exts: { if exts_js.is_string() { let str = exts_js.to_utf8(global)?; if str.slice() == b"*" { - break 'exts &[] as &[&[u8]]; + break 'exts Vec::new(); } } else if exts_js.is_array() { let mut it_2 = exts_js.array_iterator(global)?; let mut extensions = - bun_alloc::ArenaVec::<&'static [u8]>::with_capacity_in( - exts_js.get_length(global)? as usize, - arena, - ); + Vec::with_capacity(exts_js.get_length(global)? as usize); while let Some(array_item) = it_2.next()? { - let slice = refs.track(array_item.to_utf8(global)?); - if slice == b"*" { + let slice = array_item.to_utf8(global)?; + if slice.slice() == b"*" { return Err(global.throw_invalid_arguments(format_args!( "'extensions' cannot include \"*\" as an extension. Pass \"*\" instead of the array." ))); @@ -961,17 +624,10 @@ impl Framework { extensions.push(if slice[0] == b'.' { slice } else { - // Concatenate "." + slice into the arena. - let mut v = bun_alloc::ArenaVec::::with_capacity_in( - 1 + slice.len(), - arena, - ); - v.push(b'.'); - v.extend_from_slice(slice); - arena_erase(v.into_bump_slice()) + Utf8Bytes::Owned([b".", slice.slice()].concat()) }); } - break 'exts arena_erase(extensions.into_bump_slice()); + break 'exts extensions; } return Err(global.throw_invalid_arguments(format_args!( @@ -979,25 +635,20 @@ impl Framework { ))); } } else { - &[ + literals(&[ b".jsx", b".tsx", b".js", b".ts", b".cjs", b".cts", b".mjs", b".mts", - ] + ]) }; - let ignore_dirs: &'static [&'static [u8]] = if let Some(exts_js) = - fsr_opts.get(global, "ignoreDirs")? - { + let ignore_dirs = if let Some(exts_js) = fsr_opts.get(global, "ignoreDirs")? { 'exts: { if exts_js.is_array() { let mut it_2 = array.array_iterator(global)?; - let mut dirs = bun_alloc::ArenaVec::<&'static [u8]>::with_capacity_in( - len as usize, - arena, - ); + let mut dirs = Vec::with_capacity(len as usize); while let Some(array_item) = it_2.next()? { - dirs.push(refs.track(array_item.to_utf8(global)?)); + dirs.push(array_item.to_utf8(global)?); } - break 'exts arena_erase(dirs.into_bump_slice()); + break 'exts dirs; } return Err(global.throw_invalid_arguments(format_args!( @@ -1005,7 +656,7 @@ impl Framework { ))); } } else { - &[b".git", b"node_modules"] + literals(&[b".git", b"node_modules"]) }; file_system_router_types.push(FileSystemRouterType { @@ -1042,57 +693,16 @@ impl Framework { Ok(framework) } - /// Project the fields the bundler reads into the lower-tier - /// `bun_bundler::bake_types::Framework` view. The bundler crate cannot - /// name `bun_runtime::bake::Framework`, so it carries a TYPE_ONLY subset - /// that we populate here. - pub(crate) fn as_bundler_view(&self) -> bun_bundler::bake_types::Framework { - use bun_bundler::bake_types as bt; - let mut built_in_modules = bun_collections::StringArrayHashMap::new(); - for (k, v) in self.built_in_modules.iter() { - let bv = match *v { - BuiltInModule::Import(p) => bt::BuiltInModule::Import(p.into()), - BuiltInModule::Code(c) => bt::BuiltInModule::Code(c.into()), - }; - bun_core::handle_oom(built_in_modules.put(k, bv)); - } - let server_components = self - .server_components - .as_ref() - .map(|sc| bt::ServerComponents { - separate_ssr_graph: sc.separate_ssr_graph, - server_runtime_import: sc.server_runtime_import.into(), - server_register_client_reference: sc.server_register_client_reference.into(), - server_register_server_reference: sc.server_register_server_reference.into(), - client_register_server_reference: sc.client_register_server_reference.into(), - }); - let react_fast_refresh = self - .react_fast_refresh - .as_ref() - .map(|rfr| bt::ReactFastRefresh { - import_source: rfr.import_source.into(), - }); - bt::Framework::new( - built_in_modules, - server_components, - react_fast_refresh, - self.is_built_in_react, - ) - } - - pub(crate) fn init_transpiler_with_options<'a>( - &mut self, + /// Returns the arena slot for the `bake_types::Framework` projection, which the arena does not drop. + pub(crate) fn init_transpiler<'a>( + &self, arena: &'a Arena, log: &mut bun_ast::Log, mode: Mode, renderer: Graph, out: &mut core::mem::MaybeUninit>, bundler_options: &BuildConfigSubset, - source_map: bun_bundler::options::SourceMapOption, - minify_whitespace: Option, - minify_syntax: Option, - minify_identifiers: Option, - ) -> crate::Result<()> { + ) -> crate::Result<*mut bun_bundler::bake_types::Framework> { // `ASTMemoryAllocator::enter` returns an RAII `Scope` whose `Drop` // runs `exit()` at end-of-fn. let mut ast_memory_allocator = bun_ast::ASTMemoryAllocator::borrowing(arena); @@ -1138,7 +748,7 @@ impl Framework { out.options.conditions = bun_bundler::options::ESMConditions::init( out.options.target.default_conditions(), out.options.target.is_server_side(), - bundler_options.conditions.keys(), + &[], )?; if renderer == Graph::Server && self.server_components.is_some() { out.options.conditions.append_slice(&[b"react-server"])?; @@ -1155,26 +765,32 @@ impl Framework { out.options.production = mode != Mode::Development; out.options.tree_shaking = mode != Mode::Development; - out.options.minify_syntax = minify_syntax.unwrap_or(mode != Mode::Development); - out.options.minify_identifiers = minify_identifiers.unwrap_or(mode != Mode::Development); - out.options.minify_whitespace = minify_whitespace.unwrap_or(mode != Mode::Development); + // The dev server never minifies. + let minify = |option: Option| mode != Mode::Development && option.unwrap_or(true); + out.options.minify_syntax = minify(bundler_options.minify_syntax); + out.options.minify_identifiers = minify(bundler_options.minify_identifiers); + out.options.minify_whitespace = minify(bundler_options.minify_whitespace); out.options.css_chunking = true; // The bundler crate (lower tier) carries a TYPE_ONLY projection // (`bake_types::Framework`); construct it here and give it arena // lifetime so `BundleOptions<'a>` can borrow it for the bundle pass. - // NOTE: interior `Box<[u8]>` in the projection are not dropped by - // bumpalo — bounded per-session, revisit when `bake_types::BuiltInModule` - // is reshaped to `&'a [u8]`. - out.options.framework = Some(&*arena.alloc(self.as_bundler_view())); + let framework_view: *mut bun_bundler::bake_types::Framework = + arena.alloc(self.as_bundler_view()); + // SAFETY: `arena.alloc` returns a non-null, initialized pointer backed by `arena: &'a Arena`, + // which outlives `out: &mut Transpiler<'a>`, so borrowing it as `&'a Framework` is sound. + out.options.framework = Some(unsafe { &*framework_view }); out.options.inline_entrypoint_import_meta_main = true; - if let Some(ignore) = bundler_options.ignore_dce_annotations { - out.options.ignore_dce_annotations = ignore; - } - out.options.source_map = source_map; + out.options.source_map = match mode { + // DevServer links source maps itself and relies on them always being enabled. + Mode::Development => bun_bundler::options::SourceMapOption::External, + Mode::ProductionStatic => { + bun_bundler::options::SourceMapOption::from_api(Some(bundler_options.source_map)) + } + }; if bundler_options.env != bun_schema::api::DotEnvBehavior::_none { out.options.env.behavior = bundler_options.env; - out.options.env.prefix = bundler_options.env_prefix.unwrap_or(b"").into(); + out.options.env.prefix = bundler_options.env_prefix.clone().unwrap_or_default(); } // The resolver crate carries a FORWARD_DECL subset of // `BundleOptions`, so re-project via the dedicated helper rather than @@ -1195,7 +811,7 @@ impl Framework { }, )?; - if (bundler_options.define.keys.len() + bundler_options.drop.count()) > 0 { + if !bundler_options.define.keys.is_empty() { debug_assert_eq!( bundler_options.define.keys.len(), bundler_options.define.values.len() @@ -1211,15 +827,6 @@ impl Framework { bun_bundler::defines::DefineData::parse(k, v, false, false, log, arena)?; out.options.define.insert(k, parsed)?; } - - for drop_item in bundler_options.drop.keys() { - if !drop_item.is_empty() { - let parsed = bun_bundler::defines::DefineData::parse( - drop_item, b"", true, true, log, arena, - )?; - out.options.define.insert(drop_item, parsed)?; - } - } } if mode != Mode::Development { @@ -1232,71 +839,18 @@ impl Framework { // Re-sync after define/naming mutations so the resolver sees the // final option set. out.sync_resolver_opts(); - Ok(()) + Ok(framework_view) } } -#[derive(Clone)] -pub(crate) struct FileSystemRouterType { - pub root: &'static [u8], - pub prefix: &'static [u8], - pub entry_server: &'static [u8], - pub entry_client: Option<&'static [u8]>, - pub ignore_underscores: bool, - pub ignore_dirs: &'static [&'static [u8]], - pub extensions: &'static [&'static [u8]], - pub style: framework_router::Style, - pub allow_layouts: bool, -} - -#[derive(Clone, Copy)] -pub enum BuiltInModule { - Import(&'static [u8]), - Code(&'static [u8]), -} - -#[derive(Copy, Clone)] -pub(crate) struct ServerComponents { - pub separate_ssr_graph: bool, - pub server_runtime_import: &'static [u8], - // pub client_runtime_import: &'static [u8], - pub server_register_client_reference: &'static [u8], - pub server_register_server_reference: &'static [u8], - pub client_register_server_reference: &'static [u8], -} - -impl Default for ServerComponents { - fn default() -> Self { - Self { - separate_ssr_graph: false, - server_runtime_import: b"", - server_register_client_reference: b"registerClientReference", - server_register_server_reference: b"registerServerReference", - client_register_server_reference: b"registerServerReference", - } - } -} - -#[derive(Copy, Clone)] -pub(crate) struct ReactFastRefresh { - pub import_source: &'static [u8], -} - -impl Default for ReactFastRefresh { - fn default() -> Self { - Self { - import_source: b"react-refresh/runtime", - } - } +fn literals(list: &[&'static [u8]]) -> Vec> { + list.iter().copied().map(Utf8Bytes::Borrowed).collect() } #[inline] -fn resolve_or_null(r: &mut bun_resolver::Resolver, path: &[u8]) -> Option<&'static [u8]> { - let top_level_dir = bun_resolver::fs::FileSystem::get().top_level_dir; - match r.resolve(top_level_dir, path, bun_ast::ImportKind::Stmt) { - // `path_const().text` is `&'static [u8]` already (`FilenameStore`- - // backed; see note in `resolve_helper` above and `bun_ptr::Interned`). - Ok(res) => Some(res.path_const().unwrap().text), +fn resolve_or_null(r: &mut bun_resolver::Resolver, path: &[u8]) -> Option> { + match super::resolve_path(r, path) { + Ok(resolved) => Some(resolved), Err(_) => { r.log_mut().reset(); None @@ -1311,17 +865,6 @@ fn style_from_js(value: JSValue, global: &JSGlobalObject) -> JsResult JsResult> { - Ok(target - .get_optional_slice(global, property)? - .map(|slice| allocations.track(slice))) -} - // Note: `HmrRuntime` is defined canonically in the parent `bake/mod.rs` // (struct with `code: &'static ZStr` + `line_count`); re-export so callers // using `bake_body::HmrRuntime` see the same nominal type. diff --git a/src/runtime/bake/mod.rs b/src/runtime/bake/mod.rs index 0a924e1e419b..6b050037d992 100644 --- a/src/runtime/bake/mod.rs +++ b/src/runtime/bake/mod.rs @@ -9,10 +9,11 @@ //! tracing) live in `DevServer.rs` and the other `#[path]` submodules below. use core::ptr::NonNull; -use std::borrow::Cow; + +use bun_core::Utf8Bytes; // ─── Submodule bodies ──────────────────────────────────────────────────────── -// `bake_body.rs` carries the Framework/UserOptions/BuildConfigSubset `from_js` +// `bake_body.rs` carries `UserOptions`, the Framework/BuildConfigSubset `from_js` // impls plus the `init_server_runtime`/`get_hmr_runtime` host fns. #[path = "bake_body.rs"] pub(crate) mod bake_body; @@ -34,8 +35,6 @@ mod production_body; // codegen-emitted `extern "C"` thunks in `generated_host_exports.rs`. pub(crate) mod source_provider_exports; -// Re-exports from the submodule bodies so `production.rs` can name them -// without going through the keystone stubs below. pub(crate) use bake_body::{PatternBuffer, UserOptions, print_warning}; /// All bake JSC references go through this re-export of `bun_jsc`. @@ -79,46 +78,57 @@ impl Mode { /// `bake.Framework.ServerComponents`. /// -/// String fields are arena-backed at runtime but default to static literals. -/// `Cow<'static, [u8]>` covers both without leaking. -#[derive(Clone)] +/// Here and below, `Borrowed` is a literal or a path the resolver interned, `Shared` a JS string. pub(crate) struct ServerComponents { pub(crate) separate_ssr_graph: bool, /// REQUIRED — `fromJS` throws if `serverRuntimeImportSource` is absent. - pub(crate) server_runtime_import: Cow<'static, [u8]>, - pub(crate) server_register_client_reference: Cow<'static, [u8]>, - pub(crate) server_register_server_reference: Cow<'static, [u8]>, - pub(crate) client_register_server_reference: Cow<'static, [u8]>, + pub(crate) server_runtime_import: Utf8Bytes<'static>, + pub(crate) server_register_client_reference: Utf8Bytes<'static>, + pub(crate) server_register_server_reference: Utf8Bytes<'static>, + pub(crate) client_register_server_reference: Utf8Bytes<'static>, +} + +impl ServerComponents { + pub(crate) fn new(separate_ssr_graph: bool, server_runtime_import: Utf8Bytes<'static>) -> Self { + Self { + separate_ssr_graph, + server_runtime_import, + server_register_client_reference: Utf8Bytes::Borrowed(b"registerClientReference"), + server_register_server_reference: Utf8Bytes::Borrowed(b"registerServerReference"), + client_register_server_reference: Utf8Bytes::Borrowed(b"registerServerReference"), + } + } } -// No `Default` impl — `server_runtime_import` is a required field. Callers must -// supply it explicitly (`Framework::react()` sets `"react-server-dom-bun/server"`). -#[derive(Clone)] pub(crate) struct ReactFastRefresh { - pub(crate) import_source: Cow<'static, [u8]>, + pub(crate) import_source: Utf8Bytes<'static>, +} + +impl Default for ReactFastRefresh { + fn default() -> Self { + Self { + import_source: Utf8Bytes::Borrowed(b"react-refresh/runtime"), + } + } } -/// `bake.Framework.FileSystemRouterType`. Full body (with `Style` enum and -/// `from_js`) lives in the gated `bake_body.rs` draft; only the field set -/// DevServer touches is named here. pub(crate) struct FileSystemRouterType { - pub(crate) root: Cow<'static, [u8]>, - pub(crate) prefix: Cow<'static, [u8]>, - pub(crate) entry_client: Option>, + pub(crate) root: Utf8Bytes<'static>, + pub(crate) prefix: Utf8Bytes<'static>, + pub(crate) entry_client: Option>, /// REQUIRED — `fromJS` throws if missing; `Framework.resolve` /// dereferences unconditionally. - pub(crate) entry_server: Cow<'static, [u8]>, + pub(crate) entry_server: Utf8Bytes<'static>, pub(crate) ignore_underscores: bool, - pub(crate) ignore_dirs: Vec>, - pub(crate) extensions: Vec>, + pub(crate) ignore_dirs: Vec>, + pub(crate) extensions: Vec>, pub(crate) style: framework_router::Style, pub(crate) allow_layouts: bool, } /// A "Framework" is simply a set of bundler options that a framework author -/// would set in order to integrate with the application. Since many fields -/// have default values which may point to static memory, this structure is -/// always arena-allocated, usually owned by the arena in `UserOptions`. +/// would set in order to integrate with the application. +#[derive(Default)] pub(crate) struct Framework { pub(crate) is_built_in_react: bool, /// Owned `Vec` so `resolve()` can take `&mut` and rewrite entries in @@ -129,6 +139,24 @@ pub(crate) struct Framework { pub(crate) built_in_modules: bun_collections::StringArrayHashMap, } +fn resolve_path( + r: &mut bun_resolver::Resolver, + path: &[u8], +) -> Result, bun_resolver::Error> { + let top_level_dir = bun_resolver::fs::FileSystem::get().top_level_dir; + let result = r.resolve(top_level_dir, path, bun_ast::ImportKind::Stmt)?; + // No path: disabled, by a "browser" map for example. + let text = result + .path_const() + .ok_or(bun_resolver::Error::ModuleNotFound)? + .text; + // `text` is typed `'static`, but is `path` for an external or `data:` result. + Ok(match bun_resolver::fs::as_interned_path(text) { + Some(interned) => Utf8Bytes::Borrowed(interned), + None => Utf8Bytes::Owned(text.to_vec()), + }) +} + impl Framework { /// Project the runtime-side `bake::Framework` into the bundler crate's /// TYPE_ONLY view (`bun_bundler::bake_types::Framework`). The bundler is a @@ -137,14 +165,7 @@ impl Framework { /// `out.options.framework`. fn as_bundler_view(&self) -> bun_bundler::bake_types::Framework { use bun_bundler::bake_types as bt; - let mut built_in_modules = bun_collections::StringArrayHashMap::new(); - for (k, v) in self.built_in_modules.iter() { - let bv = match v { - BuiltInModule::Import(p) => BuiltInModule::Import(p.clone()), - BuiltInModule::Code(c) => BuiltInModule::Code(c.clone()), - }; - bun_core::handle_oom(built_in_modules.put(k, bv)); - } + let built_in_modules = bun_core::handle_oom(self.built_in_modules.clone()); let server_components = self .server_components .as_ref() @@ -178,168 +199,12 @@ impl Framework { ) } - /// Sets up a per-graph - /// `Transpiler` in place. The full body lives in - /// `bake_body::Framework::init_transpiler_with_options`; this keystone - /// version operates on the keystone `BuildConfigSubset` (which omits - /// `conditions`/`env`/`define`/`drop` until the schema types are - /// const-constructible — those paths default). - /// Returns the arena slot for the `bake_types::Framework` projection; caller must `drop_in_place` it. - pub(crate) fn init_transpiler<'a>( - &mut self, - arena: &'a bun_alloc::Arena, - log: &mut bun_ast::Log, - mode: Mode, - renderer: Graph, - out: &mut core::mem::MaybeUninit>, - bundler_options: &BuildConfigSubset, - ) -> crate::Result<*mut bun_bundler::bake_types::Framework> { - use bun_options_types::schema as bun_schema; - - let mut ast_memory_allocator = bun_ast::ASTMemoryAllocator::borrowing(arena); - let _ast_scope = ast_memory_allocator.enter(); - - let out: &mut bun_bundler::Transpiler = out.write(bun_bundler::Transpiler::init( - arena, - log, - bun_schema::api::TransformOptions::default(), - None, - )?); - - out.options.target = match renderer { - Graph::Client => bun_ast::Target::Browser, - Graph::Server | Graph::Ssr => bun_ast::Target::Bun, - }; - out.options.public_path = match renderer { - Graph::Client => dev_server::CLIENT_PREFIX.as_bytes().into(), - Graph::Server | Graph::Ssr => Box::default(), - }; - out.options.entry_points = Box::default(); - out.options.log = log; - out.options.output_format = mode.output_format(); - out.options.out_extensions = bun_collections::StringHashMap::new(); - out.options.hot_module_reloading = mode == Mode::Development; - out.options.code_splitting = mode != Mode::Development; - out.options.output_dir = Box::default(); - - out.options.react_fast_refresh = mode == Mode::Development - && renderer == Graph::Client - && self.react_fast_refresh.is_some(); - out.options.server_components = self.server_components.is_some(); - - out.options.conditions = bun_bundler::options::ESMConditions::init( - out.options.target.default_conditions(), - out.options.target.is_server_side(), - bundler_options.conditions.keys(), - )?; - if renderer == Graph::Server && self.server_components.is_some() { - out.options.conditions.append_slice(&[b"react-server"])?; - } - if mode == Mode::Development { - out.options.conditions.append_slice(&[b"development"])?; - } - if matches!(renderer, Graph::Server | Graph::Ssr) { - out.options.conditions.append_slice(&[b"node"])?; - } - - out.options.production = mode != Mode::Development; - out.options.tree_shaking = mode != Mode::Development; - // The three minify overrides always default to `mode != Development` - // here regardless of `BuildConfigSubset`. User-supplied minify flags - // are only honored by `init_transpiler_with_options` (bake_body). - out.options.minify_syntax = mode != Mode::Development; - out.options.minify_identifiers = mode != Mode::Development; - out.options.minify_whitespace = mode != Mode::Development; - out.options.css_chunking = true; - // The bundler crate (lower tier) carries a TYPE_ONLY - // projection (`bake_types::Framework`); construct it here and give it - // arena lifetime so `BundleOptions<'a>` can borrow it for the bundle pass. - let framework_view: *mut bun_bundler::bake_types::Framework = - arena.alloc(self.as_bundler_view()); - // SAFETY: `arena.alloc` returns a non-null, initialized pointer backed by `arena: &'a Arena`, - // which outlives `out: &mut Transpiler<'a>`, so borrowing it as `&'a Framework` is sound. - out.options.framework = Some(unsafe { &*framework_view }); - out.options.inline_entrypoint_import_meta_main = true; - if let Some(ignore) = bundler_options.ignore_dce_annotations { - out.options.ignore_dce_annotations = ignore; - } - out.options.source_map = match mode { - // Source maps must always be external, as DevServer special cases - // the linking and part of the generation of these. It also relies - // on source maps always being enabled. - Mode::Development => bun_bundler::options::SourceMapOption::External, - // TODO: follow user configuration - Mode::ProductionStatic => bun_bundler::options::SourceMapOption::None, - }; - if bundler_options.env != bun_schema::api::DotEnvBehavior::_none { - out.options.env.behavior = bundler_options.env; - out.options.env.prefix = bundler_options.env_prefix.unwrap_or(b"").into(); - } - // The resolver crate carries a FORWARD_DECL subset of `BundleOptions`, so - // re-project via the dedicated helper rather than `Clone`. - out.sync_resolver_opts(); - - out.configure_linker(); - out.configure_defines()?; - out.options.jsx.development = mode == Mode::Development; - - bake_body::add_import_meta_defines( - &mut out.options.define, - mode, - match renderer { - Graph::Client => Side::Client, - Graph::Server | Graph::Ssr => Side::Server, - }, - )?; - - if (bundler_options.define.keys.len() + bundler_options.drop.count()) > 0 { - debug_assert_eq!( - bundler_options.define.keys.len(), - bundler_options.define.values.len() - ); - use bun_bundler::DefineDataExt; - for (k, v) in bundler_options - .define - .keys - .iter() - .zip(bundler_options.define.values.iter()) - { - let parsed = - bun_bundler::defines::DefineData::parse(k, v, false, false, log, arena)?; - out.options.define.insert(k, parsed)?; - } - - for drop_item in bundler_options.drop.keys() { - if !drop_item.is_empty() { - let parsed = bun_bundler::defines::DefineData::parse( - drop_item, b"", true, true, log, arena, - )?; - out.options.define.insert(drop_item, parsed)?; - } - } - } - - if mode != Mode::Development { - // Hide information about the source repository, at the cost of debugging quality. - out.options.entry_naming = b"_bun/[hash].[ext]".as_slice().into(); - out.options.chunk_naming = b"_bun/[hash].[ext]".as_slice().into(); - out.options.asset_naming = b"_bun/[hash].[ext]".as_slice().into(); - } - - // Re-sync after define/naming mutations so the - // resolver sees the final option set. - out.sync_resolver_opts(); - Ok(framework_view) - } - /// Resolves built-in module - /// specifiers and entry points against the resolvers; returns a clone - /// with resolved paths. Errors written into `r.log`. + /// specifiers and entry points against the resolvers, in place. Errors written into `r.log`. pub(crate) fn resolve( &mut self, server: &mut bun_resolver::Resolver, client: &mut bun_resolver::Resolver, - arena: &bun_alloc::Arena, ) -> crate::Result<()> { let mut had_errors = false; @@ -363,14 +228,13 @@ impl Framework { } for fsr in self.file_system_router_types.iter_mut() { let top_level_dir = bun_resolver::fs::FileSystem::get().top_level_dir; - fsr.root = Cow::Owned( + fsr.root = Utf8Bytes::Owned( bun_paths::resolve_path::join_abs::( top_level_dir, &fsr.root, ) .to_vec(), ); - let _ = arena; if let Some(entry_client) = &mut fsr.entry_client { Self::resolve_helper( &self.built_in_modules, @@ -398,22 +262,18 @@ impl Framework { fn resolve_helper( built_in_modules: &bun_collections::StringArrayHashMap, r: &mut bun_resolver::Resolver, - path: &mut Cow<'static, [u8]>, + path: &mut Utf8Bytes<'static>, had_errors: &mut bool, desc: &[u8], ) { if let Some(module) = built_in_modules.get(path) { if let BuiltInModule::Import(p) = module { - *path = Cow::Owned(p.to_vec()); + *path = Utf8Bytes::Owned(p.to_vec()); } return; } - let top_level_dir = bun_resolver::fs::FileSystem::get().top_level_dir; - match r.resolve(top_level_dir, path, bun_ast::ImportKind::Stmt) { - Ok(mut result) => { - let p = result.path().expect("just resolved"); - *path = Cow::Owned(p.text.to_vec()); - } + match resolve_path(r, path) { + Ok(resolved) => *path = resolved, Err(err) => { // This routes through `Output::err` (stderr), not // `r.log`. The "Errors written into r.log" doc on `Framework.resolve` @@ -457,118 +317,31 @@ pub(crate) struct SplitBundlerOptions { pub(crate) ssr: BuildConfigSubset, } -// ─── bake_body → keystone bridges ──────────────────────────────────────────── -// LAYERING: `UserOptions` (bake_body.rs) carries `&'static [u8]`-backed -// duplicates of `Framework`/`SplitBundlerOptions`; `DevServer::Options` -// (DevServer.rs) wants the keystone Cow-backed types defined above. Until the -// two struct families unify (tracked by the `convert_file_system_router_type` -// note in ServerConfig.rs), bridge by-value here so `server/mod.rs` can hand -// `config.bake` straight into `DevServer::init`. All `&'static [u8]` → -// `Cow::Borrowed` / `Box<[u8]>` projections are by-reference (no copy of the -// underlying arena bytes). -impl From for FileSystemRouterType { - fn from(src: bake_body::FileSystemRouterType) -> Self { - Self { - root: Cow::Borrowed(src.root), - prefix: Cow::Borrowed(src.prefix), - entry_client: src.entry_client.map(Cow::Borrowed), - entry_server: Cow::Borrowed(src.entry_server), - ignore_underscores: src.ignore_underscores, - ignore_dirs: src.ignore_dirs.iter().map(|s| Cow::Borrowed(*s)).collect(), - extensions: src.extensions.iter().map(|s| Cow::Borrowed(*s)).collect(), - style: src.style, - allow_layouts: src.allow_layouts, - } - } -} -impl From for ServerComponents { - fn from(src: bake_body::ServerComponents) -> Self { - Self { - separate_ssr_graph: src.separate_ssr_graph, - server_runtime_import: Cow::Borrowed(src.server_runtime_import), - server_register_client_reference: Cow::Borrowed(src.server_register_client_reference), - server_register_server_reference: Cow::Borrowed(src.server_register_server_reference), - client_register_server_reference: Cow::Borrowed(src.client_register_server_reference), - } - } -} -impl From for ReactFastRefresh { - fn from(src: bake_body::ReactFastRefresh) -> Self { - Self { - import_source: Cow::Borrowed(src.import_source), - } - } -} -impl From for BuiltInModule { - fn from(src: bake_body::BuiltInModule) -> Self { - match src { - bake_body::BuiltInModule::Import(p) => BuiltInModule::Import(p.into()), - bake_body::BuiltInModule::Code(c) => BuiltInModule::Code(c.into()), - } - } -} -impl From for Framework { - fn from(src: bake_body::Framework) -> Self { - let mut built_in_modules = bun_collections::StringArrayHashMap::new(); - for (k, v) in src.built_in_modules.iter() { - bun_core::handle_oom(built_in_modules.put(*k, BuiltInModule::from(*v))); - } - Self { - is_built_in_react: src.is_built_in_react, - file_system_router_types: src - .file_system_router_types - .into_iter() - .map(FileSystemRouterType::from) - .collect(), - server_components: src.server_components.map(ServerComponents::from), - react_fast_refresh: src.react_fast_refresh.map(ReactFastRefresh::from), - built_in_modules, - } - } -} -impl From for BuildConfigSubset { - fn from(src: bake_body::BuildConfigSubset) -> Self { - // `BuildConfigSubset` mirrors the field-set - // `Framework::init_transpiler` reads (everything except `source_map`, - // which only `init_transpiler_with_options` honours). - Self { - ignore_dce_annotations: src.ignore_dce_annotations, - conditions: src.conditions, - drop: src.drop, - env: src.env, - env_prefix: src.env_prefix, - define: src.define, - } - } -} -impl From for SplitBundlerOptions { - fn from(src: bake_body::SplitBundlerOptions) -> Self { - Self { - // `bake_body::Plugin` and keystone `jsc::Plugin` both alias - // `crate::api::js_bundler::Plugin` — same nominal type, no cast. - plugin: src.plugin, - client: src.client.into(), - server: src.server.into(), - ssr: src.ssr.into(), - } - } -} - -/// `bake.SplitBundlerOptions.BuildConfigSubset`. Full body (with `from_js`) -/// lives in `bake_body.rs`; this keystone mirror carries every field that -/// `Framework::init_transpiler` reads so DevServer's -/// per-graph transpilers see bunfig `[serve.static]` define/env/conditions. -#[derive(Default)] pub(crate) struct BuildConfigSubset { - pub(crate) ignore_dce_annotations: Option, - pub(crate) conditions: bun_collections::ArrayHashMap<&'static [u8], ()>, - pub(crate) drop: bun_collections::ArrayHashMap<&'static [u8], ()>, pub(crate) env: bun_options_types::schema::api::DotEnvBehavior, - pub(crate) env_prefix: Option<&'static [u8]>, + pub(crate) env_prefix: Option>, pub(crate) define: bun_options_types::schema::api::StringMap, - // `source_map` intentionally omitted — only - // `init_transpiler_with_options` (bake_body) honours it, and DevServer - // never calls that path. + pub(crate) source_map: bun_options_types::schema::api::SourceMapMode, + + pub(crate) minify_syntax: Option, + pub(crate) minify_identifiers: Option, + pub(crate) minify_whitespace: Option, +} + +impl Default for BuildConfigSubset { + fn default() -> Self { + use bun_options_types::schema::api; + BuildConfigSubset { + env: api::DotEnvBehavior::_none, + env_prefix: None, + define: api::StringMap::EMPTY, + source_map: api::SourceMapMode::External, + + minify_syntax: None, + minify_identifiers: None, + minify_whitespace: None, + } + } } /// `bake.HmrRuntime` — embedded HMR runtime code + precomputed line count. @@ -589,8 +362,6 @@ pub(crate) use bake_body::get_hmr_runtime; // NUL-terminated `&ZStr` form for JSC handoff; the bundler-side one is plain // `&[u8]`.) -pub(crate) use bake_body::StringRefList; - // ══════════════════════════════════════════════════════════════════════════ // FrameworkRouter // ══════════════════════════════════════════════════════════════════════════ diff --git a/src/runtime/bake/production.rs b/src/runtime/bake/production.rs index 9fe7bf62d261..6c6e5136799f 100644 --- a/src/runtime/bake/production.rs +++ b/src/runtime/bake/production.rs @@ -16,7 +16,7 @@ use crate::bake::framework_router::{self, FrameworkRouter, OpaqueFileId}; use bun_alloc::Arena; use bun_bundler::BundleV2; use bun_bundler::Transpiler; -use bun_bundler::options::{self as bundler_options, OutputFile, SourceMapOption}; +use bun_bundler::options::{self as bundler_options, OutputFile}; use bun_bundler::output_file::Index as OutputFileIndex; use bun_collections::{AutoBitSet, StringArrayHashMap}; @@ -418,7 +418,7 @@ fn build_with_vm(ctx: Context, cwd: &[u8], pt: &mut PerThread) -> crate::Result< loader.map.put(b"NODE_ENV", b"production")?; dotenv::set_instance(std::ptr::from_mut::(loader)); - // In-place init via `MaybeUninit` (`init_transpiler_with_options` + // In-place init via `MaybeUninit` (`init_transpiler` // keeps the out-param shape shared with the dev-server path). let mut client_transpiler = MaybeUninit::::uninit(); let mut server_transpiler = MaybeUninit::::uninit(); @@ -426,47 +426,35 @@ fn build_with_vm(ctx: Context, cwd: &[u8], pt: &mut PerThread) -> crate::Result< // `vm.log` is set from `ctx.log` (non-null, process-lifetime); // `log_mut()` is the safe accessor encapsulating the NonNull deref. let vm_log = vm.log_mut().unwrap(); - framework.init_transpiler_with_options( + framework.init_transpiler( &options.arena, vm_log, bake_body::Mode::ProductionStatic, bake_body::Graph::Server, &mut server_transpiler, &options.bundler_options.server, - SourceMapOption::from_api(Some(options.bundler_options.server.source_map)), - options.bundler_options.server.minify_whitespace, - options.bundler_options.server.minify_syntax, - options.bundler_options.server.minify_identifiers, )?; - framework.init_transpiler_with_options( + framework.init_transpiler( &options.arena, vm_log, bake_body::Mode::ProductionStatic, bake_body::Graph::Client, &mut client_transpiler, &options.bundler_options.client, - SourceMapOption::from_api(Some(options.bundler_options.client.source_map)), - options.bundler_options.client.minify_whitespace, - options.bundler_options.client.minify_syntax, - options.bundler_options.client.minify_identifiers, )?; if separate_ssr_graph { - framework.init_transpiler_with_options( + framework.init_transpiler( &options.arena, vm_log, bake_body::Mode::ProductionStatic, bake_body::Graph::Ssr, &mut ssr_transpiler, &options.bundler_options.ssr, - SourceMapOption::from_api(Some(options.bundler_options.ssr.source_map)), - options.bundler_options.ssr.minify_whitespace, - options.bundler_options.ssr.minify_syntax, - options.bundler_options.ssr.minify_identifiers, )?; } - // SAFETY: written above by init_transpiler_with_options. + // SAFETY: written above by init_transpiler. let server_transpiler = unsafe { server_transpiler.assume_init_mut() }; - // SAFETY: written above by init_transpiler_with_options. + // SAFETY: written above by init_transpiler. let client_transpiler = unsafe { client_transpiler.assume_init_mut() }; // `ssr_transpiler` stays `MaybeUninit` and is only `assume_init_mut()`'d // inside `if separate_ssr_graph` blocks below — Rust forbids forming @@ -476,7 +464,7 @@ fn build_with_vm(ctx: Context, cwd: &[u8], pt: &mut PerThread) -> crate::Result< let mut targets: Vec<&mut Transpiler> = vec![&mut *client_transpiler, &mut *server_transpiler]; if separate_ssr_graph { - // SAFETY: written above by init_transpiler_with_options when separate_ssr_graph. + // SAFETY: written above by init_transpiler when separate_ssr_graph. targets.push(unsafe { ssr_transpiler.assume_init_mut() }); } for transpiler in targets { @@ -498,27 +486,24 @@ fn build_with_vm(ctx: Context, cwd: &[u8], pt: &mut PerThread) -> crate::Result< // these share pointers right now, so setting NODE_ENV == production on one should affect all debug_assert!(core::ptr::eq(server_transpiler.env, client_transpiler.env)); - *framework = match framework.resolve( - &mut server_transpiler.resolver, - &mut client_transpiler.resolver, - &options.arena, - ) { - Ok(f) => f, - Err(_) => { - if framework.is_built_in_react { - // SAFETY: `server_transpiler.log` is the process-lifetime ctx.log. - bake_body::Framework::add_react_install_command_note(unsafe { - &mut *server_transpiler.log - })?; - } - bun_core::err_generic!("Failed to resolve all imports required by the framework"); - Output::flush(); - let _ = server_transpiler - .log() - .print(std::ptr::from_mut(Output::error_writer())); - Global::crash(); + if framework + .resolve( + &mut server_transpiler.resolver, + &mut client_transpiler.resolver, + ) + .is_err() + { + if framework.is_built_in_react { + // SAFETY: `server_transpiler.log` is the process-lifetime ctx.log. + bake::Framework::add_react_install_command_note(unsafe { &mut *server_transpiler.log }); } - }; + bun_core::err_generic!("Failed to resolve all imports required by the framework"); + Output::flush(); + let _ = server_transpiler + .log() + .print(std::ptr::from_mut(Output::error_writer())); + Global::crash(); + } bun_core::pretty_errorln!("Bundling routes"); Output::flush(); @@ -544,7 +529,7 @@ fn build_with_vm(ctx: Context, cwd: &[u8], pt: &mut PerThread) -> crate::Result< }; for fsr in &framework.file_system_router_types { - let joined_root = resolve_path::join_abs::(cwd, fsr.root); + let joined_root = resolve_path::join_abs::(cwd, &fsr.root); let Some(entry) = server_transpiler .resolver .read_dir_info_ignore_error(joined_root) @@ -552,8 +537,8 @@ fn build_with_vm(ctx: Context, cwd: &[u8], pt: &mut PerThread) -> crate::Result< continue; }; let server_file = - entry_points.get_or_put_entry_point(fsr.entry_server, bake::Side::Server)?; - let client_file = if let Some(client) = fsr.entry_client { + entry_points.get_or_put_entry_point(&fsr.entry_server, bake::Side::Server)?; + let client_file = if let Some(client) = &fsr.entry_client { Some(entry_points.get_or_put_entry_point(client, bake::Side::Client)?) } else { None @@ -566,12 +551,12 @@ fn build_with_vm(ctx: Context, cwd: &[u8], pt: &mut PerThread) -> crate::Result< ignore_dirs: fsr .ignore_dirs .iter() - .map(|s| Box::<[u8]>::from(*s)) + .map(|s| Box::<[u8]>::from(&**s)) .collect(), extensions: fsr .extensions .iter() - .map(|s| Box::<[u8]>::from(*s)) + .map(|s| Box::<[u8]>::from(&**s)) .collect(), style: fsr.style, allow_layouts: fsr.allow_layouts, @@ -587,11 +572,9 @@ fn build_with_vm(ctx: Context, cwd: &[u8], pt: &mut PerThread) -> crate::Result< framework_router::InsertionContext::wrap(&mut entry_points), )?; - // `bake_body::Framework` is the runtime-side superset; the bundler reads only + // `bake::Framework` is the runtime-side superset; the bundler reads only // `built_in_modules` / `server_components` / `react_fast_refresh` / // `is_built_in_react` via its lower-tier `bake_types::Framework` view. - // Project once here via the shared helper so the field-shape (e.g. - // `BuiltInModule` `&'static [u8]` → `Box<[u8]>`) stays in one place. // (The two Framework types could only merge if `FileSystemRouterType` / // `framework_router::Style` moved down to bun_bundler.) let bundler_framework = framework.as_bundler_view(); @@ -604,7 +587,7 @@ fn build_with_vm(ctx: Context, cwd: &[u8], pt: &mut PerThread) -> crate::Result< let server_ptr: *mut Transpiler = &raw mut *server_transpiler; let client_ptr: *mut Transpiler = &raw mut *client_transpiler; let ssr_ptr: *mut Transpiler = if separate_ssr_graph { - // SAFETY: written above by init_transpiler_with_options when separate_ssr_graph. + // SAFETY: written above by init_transpiler when separate_ssr_graph. core::ptr::from_mut(unsafe { ssr_transpiler.assume_init_mut() }) } else { server_ptr diff --git a/src/runtime/server/ServerConfig.rs b/src/runtime/server/ServerConfig.rs index 0dacf2664f04..5975a7906bf5 100644 --- a/src/runtime/server/ServerConfig.rs +++ b/src/runtime/server/ServerConfig.rs @@ -575,46 +575,6 @@ fn get_routes_object(global: &JSGlobalObject, arg: JSValue) -> JsResult crate::bake::bake_body::FileSystemRouterType { - use crate::bake::bake_body as bb; - // NOTE: `bb::arena_erase` is the single sanctioned `'bump → 'static` - // erasure for the `UserOptions.arena` self-referential pattern; bake_body's - // own `Framework::from_js` / `resolve` use it identically. - // TODO(refactor): thread a real `'bump` through `bb::Framework`/ - // `bb::FileSystemRouterType` and remove this together with `arena_erase`. - fn dupe(arena: &bun_alloc::Arena, bytes: &[u8]) -> &'static [u8] { - bb::arena_erase(arena.alloc_slice_copy(bytes)) - } - fn dupe_slice_of( - arena: &bun_alloc::Arena, - v: &[std::borrow::Cow<'static, [u8]>], - ) -> &'static [&'static [u8]] { - let inner: Vec<&'static [u8]> = v.iter().map(|c| dupe(arena, c.as_ref())).collect(); - bb::arena_erase(arena.alloc_slice_copy(&inner)) - } - - bb::FileSystemRouterType { - root: dupe(arena, src.root.as_ref()), - prefix: dupe(arena, src.prefix.as_ref()), - entry_server: dupe(arena, src.entry_server.as_ref()), - entry_client: src.entry_client.as_deref().map(|b| dupe(arena, b)), - ignore_underscores: src.ignore_underscores, - ignore_dirs: dupe_slice_of(arena, &src.ignore_dirs), - extensions: dupe_slice_of(arena, &src.extensions), - style: src.style, - allow_layouts: src.allow_layouts, - } -} - impl ServerConfig { pub(crate) fn from_js( global: &JSGlobalObject, @@ -747,18 +707,12 @@ impl ServerConfig { // iter drops at scope end let mut init_ctx_ = ServerInitContext { - // NOTE: bake owns the arena (created below and moved into - // `UserOptions`). dedupe_html_bundle_map: Default::default(), framework_router_list: Vec::new(), - js_string_allocations: crate::bake::StringRefList::EMPTY, user_routes: &mut args.static_routes, global, }; let init_ctx = &mut init_ctx_; - // arena/Vec are owned locals; drop on `?` automatically. Ownership - // transfers to args.bake on the success path via mem::take below. - // (dedupe_html_bundle_map is unused on the success path; drops at scope end.) // Vec drops elements (which deref route) // automatically on error. @@ -932,57 +886,34 @@ impl ServerConfig { || !init_ctx.framework_router_list.is_empty() { if args.development.is_hmr_enabled() { - use crate::bake::bake_body as bb; use bun_options_types::schema::api::DotEnvBehavior; - // NOTE: the arena is created here and moved into - // `UserOptions` (lives until `args.bake` is dropped). - let arena = bun_alloc::Arena::new(); - - let root = bb::arena_dupe_z( - &arena, - bun_paths::fs::FileSystem::instance().top_level_dir(), - ); - - // Convert `crate::bake::FileSystemRouterType` (Cow-backed) - // into `bake_body::FileSystemRouterType` (`&'static` slices) - // by duping every string into the arena. Type - // duplication; remove once the two structs unify. - let router_types: Vec = - core::mem::take(&mut init_ctx.framework_router_list) - .into_iter() - .map(|t| convert_file_system_router_type(&arena, &t)) - .collect(); - // SAFETY: `bun_vm()` returns the live VM for this global; // we need `&mut Resolver` for `Framework::auto`. let resolver = &mut global.bun_vm().as_mut().transpiler.resolver; - let framework = bb::Framework::auto(&arena, resolver, router_types) - .map_err(|e| global.throw_error(e, "Framework::auto"))?; + let framework = crate::bake::Framework::auto( + resolver, + core::mem::take(&mut init_ctx.framework_router_list), + ); let mut user_options = crate::bake::UserOptions { - arena, - _allocations: core::mem::replace( - &mut init_ctx.js_string_allocations, - crate::bake::StringRefList::EMPTY, + arena: bun_alloc::Arena::new(), + root: bun_core::ZBox::from_bytes( + bun_paths::fs::FileSystem::instance().top_level_dir(), ), - root, framework, - bundler_options: bb::SplitBundlerOptions::default(), + bundler_options: Default::default(), }; let o = &vm.transpiler.options.transform_options; match o.serve_env_behavior { DotEnvBehavior::prefix => { - // NOTE: `serve_env_prefix` is `Option>` - // owned by the long-lived `transform_options`; dupe - // into the arena so the `&'static [u8]` field is - // backed by `UserOptions.arena`. - user_options.bundler_options.client.env_prefix = o - .serve_env_prefix - .as_deref() - .map(|p| bb::arena_dupe_z(&user_options.arena, p).as_bytes()); + user_options + .bundler_options + .client + .env_prefix + .clone_from(&o.serve_env_prefix); user_options.bundler_options.client.env = DotEnvBehavior::prefix; } DotEnvBehavior::load_all => { diff --git a/src/runtime/server/mod.rs b/src/runtime/server/mod.rs index 5cb83b6cf091..b54dadbf53a7 100644 --- a/src/runtime/server/mod.rs +++ b/src/runtime/server/mod.rs @@ -2232,7 +2232,7 @@ impl NewServer { (*server).any_server_packed = AnyServer::from(server.cast_const()).to_packed() as usize; } - // The bake options (and the arena that backs `root`) live in + // The bake options (`root`, and the arena the transpilers live in) are in // `(*server).config.bake` for the server's lifetime. Initialise // DevServer AFTER the server box exists so the `Options::arena` borrow // points into the heap-allocated config rather than the caller's @@ -2248,15 +2248,11 @@ impl NewServer { }; let dev = match crate::bake::DevServer::init(crate::bake::DevServer::Options { arena: &bake_options.arena, - root: bake_options.root, + root: bake_options.root.as_zstr(), // SAFETY: per-thread VM singleton; STATIC lifetime. vm: jsc::VirtualMachine::get(), - // LAYERING: `UserOptions` carries the `bake_body` shapes; - // `DevServer::Options` consumes the keystone shapes; - // `From` impls in `bake/mod.rs` bridge - // until the duplicates are collapsed. - framework: core::mem::take(&mut bake_options.framework).into(), - bundler_options: core::mem::take(&mut bake_options.bundler_options).into(), + framework: core::mem::take(&mut bake_options.framework), + bundler_options: core::mem::take(&mut bake_options.bundler_options), broadcast_console_log_from_browser_to_server: broadcast, }) { Ok(d) => d, diff --git a/src/runtime/server/server_body.rs b/src/runtime/server/server_body.rs index 102a1c6ca94e..660617af4782 100644 --- a/src/runtime/server/server_body.rs +++ b/src/runtime/server/server_body.rs @@ -18,7 +18,7 @@ use crate::webcore::{ }; use ::bstr::BStr; use bun_collections::HashMap; -use bun_core::{EncodedSlice, String as BunString, strings}; +use bun_core::{EncodedSlice, String as BunString, Utf8Bytes, strings}; use bun_core::{Output, fmt as bun_fmt}; use bun_http::{self as http, Method, MimeType}; use bun_jsc::Debugger::DebuggerId; @@ -791,8 +791,6 @@ impl AnyRoute { if argument.is_object() { if let Some(dir) = argument.get_optional_slice(global, b"dir")? { - let relative_root = init_ctx.js_string_allocations.track(dir); - if !strings::ends_with(path, b"/*") { return Err(global.throw_invalid_arguments(format_args!( "To mount a directory, make sure the path ends in `/*`" @@ -821,12 +819,8 @@ impl AnyRoute { let stat_cache = argument .get_boolean_loose(global, b"statCache")? .unwrap_or(true); - let route = super::DirectoryRoute::create( - global, - relative_root, - url_prefix, - stat_cache, - )?; + let route = + super::DirectoryRoute::create(global, &dir, url_prefix, stat_cache)?; return Ok(Some(AnyRoute::Directory(route))); } @@ -835,35 +829,28 @@ impl AnyRoute { // Style impls Drop; `?` drops it on the error path. // trim the /* - // NOTE: `FileSystemRouterType` fields are `Cow<'static,[u8]>`. - // Rather - // than erasing a lifetime through a raw-pointer round-trip - // (banned per PORTING.md), copy the prefix bytes here — the - // route table is built once at server startup, so the extra - // allocation is cold. - use std::borrow::Cow; - let prefix: Cow<'static, [u8]> = if path.len() == 2 { - Cow::Borrowed(b"/") + let prefix = if path.len() == 2 { + Utf8Bytes::Borrowed(b"/") } else { - Cow::Owned(path[..path.len() - 2].to_vec()) + Utf8Bytes::Owned(path[..path.len() - 2].to_vec()) }; init_ctx .framework_router_list .push(bake::FileSystemRouterType { - root: Cow::Owned(relative_root.to_vec()), + root: dir, style, prefix, // TODO: customizable framework option. - entry_client: Some(Cow::Borrowed(b"bun-framework-react/client.tsx")), - entry_server: Cow::Borrowed(b"bun-framework-react/server.tsx"), + entry_client: Some(Utf8Bytes::Borrowed(b"bun-framework-react/client.tsx")), + entry_server: Utf8Bytes::Borrowed(b"bun-framework-react/server.tsx"), ignore_underscores: true, ignore_dirs: vec![ - Cow::Borrowed(b"node_modules".as_slice()), - Cow::Borrowed(b".git".as_slice()), + Utf8Bytes::Borrowed(b"node_modules".as_slice()), + Utf8Bytes::Borrowed(b".git".as_slice()), ], extensions: vec![ - Cow::Borrowed(b".tsx".as_slice()), - Cow::Borrowed(b".jsx".as_slice()), + Utf8Bytes::Borrowed(b".tsx".as_slice()), + Utf8Bytes::Borrowed(b".jsx".as_slice()), ], allow_layouts: true, }); @@ -893,7 +880,6 @@ impl AnyRoute { pub(crate) struct ServerInitContext<'a> { pub(crate) dedupe_html_bundle_map: HashMap<*const HTMLBundle, bun_ptr::BackRef>, - pub(crate) js_string_allocations: bake::StringRefList, pub global: &'a JSGlobalObject, pub(crate) framework_router_list: Vec, pub(crate) user_routes: &'a mut Vec, diff --git a/test/bake/app-options.test.ts b/test/bake/app-options.test.ts index af8da494bb3e..f9cf2159749e 100644 --- a/test/bake/app-options.test.ts +++ b/test/bake/app-options.test.ts @@ -57,3 +57,34 @@ test("Bun.serve({ app }) rejects wrong-typed framework options", async () => { expect(stderr).toBe(""); expect(exitCode).toBe(0); }); + +test('Bun.serve({ app }) reports a framework entry point that a "browser" map disables', async () => { + using dir = tempDir("bake-app-disabled-entry", { + "package.json": JSON.stringify({ name: "app", browser: { "./client.ts": false } }), + "client.ts": `export {};`, + "server.ts": `export function render() { return new Response("ok"); }`, + "routes/index.ts": `export default () => new Response("ok");`, + "fixture.ts": ` + const fsr = { root: "routes", style: "nextjs-pages", serverEntryPoint: "./server.ts", clientEntryPoint: "./client.ts" }; + try { + Bun.serve({ port: 0, development: true, app: { framework: { fileSystemRouterTypes: [fsr] } } }).stop(true); + console.log("no error"); + } catch (e) { + console.log("threw: " + e.message); + } + `, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "fixture.ts"], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stderr).toContain("Failed to resolve './client.ts' for framework (client side entrypoint)"); + expect(stdout).toBe("threw: Framework is missing required files!\n"); + expect(exitCode).toBe(0); +}); diff --git a/test/bake/dev/production.test.ts b/test/bake/dev/production.test.ts index 76d04d2e876a..5bf536698448 100644 --- a/test/bake/dev/production.test.ts +++ b/test/bake/dev/production.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; import { existsSync } from "fs"; -import { bunEnv, bunExe } from "harness"; +import { bunEnv, bunExe, tempDir } from "harness"; import path from "path"; import { tempDirWithBakeDeps } from "../bake-harness"; @@ -367,6 +367,26 @@ export default function Docs() { expect(stderr.toString()).toContain("Multiple pages matching the same route pattern is ambiguous"); }); + test('a framework entry point that a "browser" map disables is reported', async () => { + using dir = tempDir("bake-production-disabled-entry", { + "package.json": JSON.stringify({ name: "app", browser: { "./client.ts": false } }), + "client.ts": `export {};`, + "server.ts": `export function render() { return new Response("ok"); }`, + "routes/index.ts": `export default () => new Response("ok");`, + "app.ts": ` + const fsr = { root: "routes", style: "nextjs-pages", serverEntryPoint: "./server.ts", clientEntryPoint: "./client.ts" }; + export default { app: { framework: { fileSystemRouterTypes: [fsr] } } }; + `, + }); + + const { exitCode, stderr } = await Bun.$`${bunExe()} build --app ./app.ts` + .cwd(String(dir)) + .env(bunEnv) + .throws(false); + expect(stderr.toString()).toContain("Failed to resolve './client.ts' for framework (client side entrypoint)"); + expect(exitCode).toBe(1); + }); + test("handles build with no pages directory without crashing", async () => { const dir = await tempDirWithBakeDeps("bake-production-no-pages", { "app.ts": `export default { app: { framework: "react" } };`,