diff --git a/src/bun_core/feature_flags.rs b/src/bun_core/feature_flags.rs index cd22db7be52c..10d5b4fb4f3b 100644 --- a/src/bun_core/feature_flags.rs +++ b/src/bun_core/feature_flags.rs @@ -122,14 +122,3 @@ pub fn is_libdeflate_enabled() -> bool { !feature_flag::BUN_FEATURE_FLAG_NO_LIBDEFLATE.get() } - -/// Enable the "app" option in Bun.serve. This option will likely be removed -/// in favor of HTML loaders and configuring framework options in bunfig.toml -pub fn bake() -> bool { - // In canary or if an environment variable is specified. - env::IS_CANARY || env::IS_DEBUG || feature_flag::BUN_FEATURE_FLAG_EXPERIMENTAL_BAKE.get() -} - -/// Additional debugging features for bake.DevServer, such as the incremental visualizer. -/// To use them, extra flags are passed in addition to this one. -pub const BAKE_DEBUGGING_FEATURES: bool = env::IS_CANARY || env::IS_DEBUG; diff --git a/src/bundler/LinkerContext.rs b/src/bundler/LinkerContext.rs index bc47073ee17d..8ab1d9fcb6b5 100644 --- a/src/bundler/LinkerContext.rs +++ b/src/bundler/LinkerContext.rs @@ -12,7 +12,6 @@ use bun_sourcemap::{ }; // Note: alias the *module* (not the `ThreadPool` struct) so // `ThreadPoolLib::Task` / `ThreadPoolLib::Batch` resolve as nested items. -use crate::bake_types as bake; use bun_ast::{ImportKind, ImportRecord}; use bun_threading::{WaitGroup, thread_pool as ThreadPoolLib}; @@ -67,12 +66,21 @@ pub use crate::linker_context::static_route_visitor as StaticRouteVisitor; // `linker_context/doStep5.rs`), not free functions — no item re-export. pub(crate) use crate::linker_context::compute_cross_chunk_dependencies::compute_cross_chunk_dependencies; pub use crate::linker_context::do_step5; -pub(crate) use crate::linker_context::generate_chunks_in_parallel::generate_chunks_in_parallel; +pub use crate::linker_context::generate_chunks_in_parallel::generate_chunks_in_parallel; pub(crate) use crate::linker_context::post_process_css_chunk::post_process_css_chunk; pub(crate) use crate::linker_context::post_process_html_chunk::post_process_html_chunk; 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; +/// Subset of the framework config that chunk generation consults, projected +/// by value when `BundleV2` is set up so the linker holds no backref into the +/// framework struct. +#[derive(Copy, Clone)] +pub struct FrameworkInfo { + pub has_server_components: bool, + pub is_built_in_react: bool, +} + pub struct LinkerContext<'a> { pub(crate) parse_graph: *mut Graph<'a>, pub graph: LinkerGraph<'a>, @@ -119,17 +127,18 @@ pub struct LinkerContext<'a> { pub(crate) has_any_css_locals: AtomicU32, - /// Used by Bake to extract []CompileResult before it is joined. - /// CYCLEBREAK GENUINE: erased bake::DevServer (see bundle_v2::dispatch). - pub dev_server: Option, - pub(crate) framework: Option>, + /// True when a dev server is driving this bundle. Chunk generation only + /// branches on the fact; all dispatch goes through the erased handle on + /// `BundleV2.dev_server`, which is the single owner of that seam. + pub has_dev_server: bool, + pub framework: Option, pub(crate) mangled_props: MangledProps, } // SAFETY: `LinkerContext` is shared across the worker pool via `each_ptr` / // `SourceMapDataTask`. The raw-pointer fields (`parse_graph`, `resolver`, -// `r#loop`, `framework`) are backrefs into `BundleV2`/`Transpiler` whose +// `r#loop`) are backrefs into `BundleV2`/`Transpiler` whose // lifetimes strictly outlive every parallel section, and per-thread writes go // to disjoint SoA slots (see `compute_line_offsets`). unsafe impl<'a> Send for LinkerContext<'a> {} @@ -155,7 +164,7 @@ impl<'a> Default for LinkerContext<'a> { source_maps: Default::default(), pending_task_count: AtomicU32::new(0), has_any_css_locals: AtomicU32::new(0), - dev_server: None, + has_dev_server: false, framework: None, mangled_props: Default::default(), } @@ -688,7 +697,7 @@ impl<'a> LinkerContext<'a> { /// # Safety /// `bundle` must be valid for the call and `self` must be `(*bundle).linker`. #[inline(never)] - pub(crate) unsafe fn link( + pub unsafe fn link( &mut self, bundle: *mut BundleV2<'a>, entry_points: &[Index], @@ -2988,7 +2997,7 @@ impl<'a> LinkerContext<'a> { } /// `log` is an explicit parameter (not `self.log`) because the dev-server - /// caller (`finish_from_bake_dev_server`) runs this *before* `load()` has + /// caller (`finish_from_dev_server`) runs this *before* `load()` has /// initialized `self.log`, passing a stack-local `Log` instead. pub(crate) fn scan_css_imports( file_source_index: u32, @@ -4179,7 +4188,7 @@ impl PartialEq for MatchImport { pub struct StmtList { // Temporary scratch buffers: plain `Vec`s on the global allocator // (cleared/reused per chunk, freed by Drop). - pub(crate) inside_wrapper_prefix: InsideWrapperPrefix, + pub inside_wrapper_prefix: InsideWrapperPrefix, pub(crate) outside_wrapper_prefix: Vec, pub(crate) inside_wrapper_suffix: Vec, pub(crate) all_stmts: Vec, @@ -4211,7 +4220,10 @@ impl InsideWrapperPrefix { } impl InsideWrapperPrefix { - pub(crate) fn append_non_dependency(&mut self, stmt: Stmt) -> Result<(), AllocError> { + // `pub`: also called by the `Format::InternalBakeDev` statement + // conversion in `bun_runtime`'s bake module (see + // `crate::convert_stmts_for_chunk_hmr`). + pub fn append_non_dependency(&mut self, stmt: Stmt) -> Result<(), AllocError> { self.stmts.push(stmt); Ok(()) } @@ -4370,7 +4382,10 @@ impl StmtList { } } - pub(crate) fn append(&mut self, list: StmtListWhich, stmt: Stmt) { + // `pub`: also called by the `Format::InternalBakeDev` statement + // conversion in `bun_runtime`'s bake module (see + // `crate::convert_stmts_for_chunk_hmr`). + pub fn append(&mut self, list: StmtListWhich, stmt: Stmt) { match list { StmtListWhich::OutsideWrapperPrefix => self.outside_wrapper_prefix.push(stmt), StmtListWhich::InsideWrapperSuffix => self.inside_wrapper_suffix.push(stmt), diff --git a/src/bundler/ParseTask.rs b/src/bundler/ParseTask.rs index bf13ab135e1a..3902b4478603 100644 --- a/src/bundler/ParseTask.rs +++ b/src/bundler/ParseTask.rs @@ -14,6 +14,7 @@ use bun_ast::{Loc, Location, Log, Msg, Source}; use bun_collections::VecExt; use bun_core::strings; use bun_core::{self, FeatureFlags, declare_scope, scoped_log}; +use bun_options_types::BuiltInModule; use bun_sys::Fd; use bun_threading::thread_pool as ThreadPoolLib; @@ -1310,7 +1311,7 @@ pub mod parse_worker { write!( &mut buf, "{}/{}{}", - crate::bake_types::ASSET_PREFIX, + bun_options_types::DEV_SERVER_ASSET_PREFIX, bun_core::fmt::bytes_to_hex_lower_string(&content_hash.to_ne_bytes()), bstr::BStr::new(bun_paths::extension(source.path.text)), ) @@ -1410,7 +1411,7 @@ pub mod parse_worker { if let Some(f) = &ctx.framework { if let Some(file) = f.built_in_modules.get(file_path.text) { match file { - crate::bake_types::BuiltInModule::Code(code) => { + BuiltInModule::Code(code) => { break 'brk Ok(CacheEntry { contents: crate::cache::Contents::SharedBuffer { ptr: code.as_ptr(), @@ -1420,7 +1421,7 @@ pub mod parse_worker { ..Default::default() }); } - crate::bake_types::BuiltInModule::Import(path) => { + BuiltInModule::Import(path) => { *file_path = Fs::Path::init(path); break 'lookup_builtin; } @@ -2543,10 +2544,10 @@ pub mod parse_worker { bun_ast::runtime::ServerComponentsMode::None }; - // `transpiler.options.framework: Option<&bake_types::Framework>` - // vs `opts.framework: Option<&js_parser::options::Framework>` — both - // TYPE_ONLY mirrors of `bake.Framework`. Project the fields the parser - // reads into the parser-side mirror and bump-alloc + // `transpiler.options.framework` and `opts.framework` are distinct + // TYPE_ONLY mirrors of the runtime-side framework config (the bundler + // seam struct vs `js_parser::options::Framework`). Project the fields + // the parser reads into the parser-side mirror and bump-alloc // so `opts` can borrow it. opts.framework = topts.framework.map(|f| { // `Framework` is bump-allocated below, so `Drop` never runs — use arena-owned slices. diff --git a/src/bundler/bake_types.rs b/src/bundler/bake_types.rs new file mode 100644 index 000000000000..d9a2811ede87 --- /dev/null +++ b/src/bundler/bake_types.rs @@ -0,0 +1,97 @@ +//! CYCLEBREAK(b0) TYPE_ONLY seam module: pure value types shared between the +//! bundler internals and `bun_runtime::bake`, kept at the lower tier so the +//! bundler can consume them without depending on the full DevServer. +//! `bun_runtime::bake` re-exports these as the canonical defs and constructs +//! values of them (e.g. `Framework` is projected from the runtime-side +//! superset via `as_bundler_view`). + +#[repr(u8)] +#[derive(Copy, Clone, Eq, PartialEq, Debug, core::marker::ConstParamTy)] +pub enum Side { + Client = 0, + Server = 1, +} +#[repr(u8)] +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub enum Graph { + Client = 0, + Server = 1, + Ssr = 2, +} +/// Used for the per-file `// path (target)` comment +/// in postProcessJSChunk and friends. +impl From for &'static str { + fn from(g: Graph) -> Self { + match g { + Graph::Client => "client", + Graph::Server => "server", + Graph::Ssr => "ssr", + } + } +} +impl Side { + pub fn graph(self) -> Graph { + match self { + Side::Client => Graph::Client, + Side::Server => Graph::Server, + } + } +} +/// Bundler-only `Target` extension: which dev-server graph a file bundled for +/// that target lands in. Declared next to `Graph` because the canonical +/// `Target` lives in `bun_ast` (lower tier, cannot name seam types); callers +/// import it from here (`crate::bake_types::TargetExt`). +pub trait TargetExt: Copy { + fn bake_graph(self) -> Graph; +} +impl TargetExt for bun_ast::Target { + fn bake_graph(self) -> Graph { + use bun_ast::Target; + match self { + Target::Browser => Graph::Client, + Target::ServerComponentsSsr => Graph::Ssr, + Target::BunMacro | Target::Bun | Target::Node => Graph::Server, + } + } +} +/// Canonical definition lives in `bun_options_types` (T3); re-exported +/// here so bundler and bake (in runtime, T6) share one nominal type. +pub use bun_options_types::BuiltInModule; + +/// Bundler-owned TYPE_ONLY `Framework` view — canonical defs live in +/// `options_impl` (they are made of bundler/parser vocabulary, no bake +/// references); re-exported here so `bun_runtime::bake` keeps reaching them +/// through the seam module when projecting its canonical `bake.Framework` +/// via `as_bundler_view`. +pub use crate::options_impl::{Framework, ReactFastRefresh, ServerComponents}; + +/// Seam type: the HMR runtime preamble the linker splices ahead of each +/// `Format::InternalBakeDev` chunk. +#[derive(Clone, Copy)] +pub struct HmrRuntime { + pub code: &'static [u8], +} +/// Alias used at the crate root (`crate::HmrRuntimeSide`); identical to `Side`. +pub(crate) type HmrRuntimeSide = Side; + +/// The runtime's bytes are embedded once, by `bun_runtime`'s dev-server module +/// (which also hands them to JSC); the bundler reaches them through the +/// link-time hook below, same pattern as `__bun_bake_convert_stmts_for_chunk_hmr` +/// in `lib.rs`. Memoized per side. +pub(crate) fn get_hmr_runtime(side: Side) -> HmrRuntime { + static CLIENT: std::sync::OnceLock = std::sync::OnceLock::new(); + static SERVER: std::sync::OnceLock = std::sync::OnceLock::new(); + let cell = match side { + Side::Client => &CLIENT, + Side::Server => &SERVER, + }; + *cell.get_or_init(|| __bun_bake_get_hmr_runtime(side)) +} + +unsafe extern "Rust" { + /// Defined `#[no_mangle]` in `bun_runtime` (`bake/bake_body.rs`). All + /// argument/return types are safe Rust values (no raw-pointer + /// preconditions), so the link-time-resolved body upholds Rust's + /// invariants on its own. + safe fn __bun_bake_get_hmr_runtime(side: Side) -> HmrRuntime; +} diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index b117a316ffa7..0979eeb12fbd 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -9,22 +9,23 @@ use core::ptr::NonNull; use bun_collections::{ArrayHashMap, StringHashMap}; use bun_core::ThreadLock; -// `bake_types` / `dispatch` are canonically defined in `bv2_impl` below -// (the full versions); re-exported here so the crate-root `lib.rs` modules and -// the outer `BundleV2` struct see exactly the same types as the impl bodies. +// `dispatch` is canonically defined in `bv2_impl` below (the full version); +// re-exported here so the crate-root `lib.rs` modules and the outer +// `BundleV2` struct see exactly the same types as the impl bodies. pub use bv2_impl::api; -pub use bv2_impl::bake_types; pub use bv2_impl::dispatch; pub use bv2_impl::{ CompileResult, CompileResultForSourceMap, CompileResultForSourceMapColumns, ContentHasher, - EventLoop, ImportTracker, PartRange, StableRef, WrapKind, generic_path_with_pretty_initialized, - target_from_hashbang, + EventLoop, ImportTracker, PartRange, StableRef, WrapKind, generate_unique_key, + generic_path_with_pretty_initialized, target_from_hashbang, +}; +pub use bv2_impl::{ + DevServerInput, DevServerOutput, EntryPointFlags, EntryPointList, ImportTrackerIterator, + ImportTrackerStatus, }; -pub use bv2_impl::{DevServerInput, DevServerOutput, ImportTrackerIterator, ImportTrackerStatus}; // Flatten the impl-body module into this file's namespace so external callers // (`bun_runtime::cli::*`, `linker_context::*`) reference items as // `bundle_v2::Foo` rather than naming the implementation submodule. -use self::bake_types as bake; pub use bv2_impl::{ BuildResult, BundleV2Result, CompletionStruct, DependenciesScanner, DependenciesScannerResult, OnDependenciesAnalyze, singleton, @@ -65,25 +66,31 @@ pub struct BundleV2<'a> { /// When Server Components is enabled, this is used for the client bundles /// and `transpiler` is used for the server bundles. /// - /// `ParentRef` (not raw `NonNull`): set once in `init` (from `BakeOptions` - /// or `initialize_client_transpiler`), the pointee is live for `'a`, and - /// the read-only projection (`client_transpiler_ref`) is the common path — - /// so the safe `Deref` removes the per-accessor `unsafe { p.as_ref() }`. - /// The two `&mut` sites in `transpiler_for_target` go through the explicit - /// `unsafe assume_mut` escape hatch. + /// `ParentRef` (not raw `NonNull`): set once in `init` (from + /// `FrameworkBundleOptions` or `initialize_client_transpiler`), the + /// pointee is live for `'a`, and the read-only projection + /// (`client_transpiler_ref`) is the common path — so the safe `Deref` + /// removes the per-accessor `unsafe { p.as_ref() }`. The two `&mut` sites + /// in `transpiler_for_target` go through the explicit `unsafe assume_mut` + /// escape hatch. pub(crate) client_transpiler: Option, bun_ptr::Mut>>, /// Owns the storage backing `client_transpiler` when it was lazily created /// by `initialize_client_transpiler` (browser-target request from a /// server-side build). Stays `None` when `client_transpiler` is borrowed - /// from `BakeOptions` (DevServer owns that one). Dropped in + /// from `FrameworkBundleOptions` (the caller owns that one). Dropped in /// `deinit_without_freeing_arena` so the deep-cloned `BundleOptions` / /// `Resolver` global-heap fields are released — `arena.alloc` would leak /// them since bumpalo never runs `Drop`. pub(crate) owned_client_transpiler: Option>>, - /// See `bake.Framework.ServerComponents.separate_ssr_graph`. + /// See `ServerComponents.separate_ssr_graph` (`crate::options`). pub(crate) ssr_transpiler: *mut Transpiler<'a>, - /// When Bun Bake is used, the resolved framework is passed here. - pub(crate) framework: Option, + /// Framework configuration for multi-graph (server components / fast + /// refresh) builds, supplied through `FrameworkBundleOptions`. + pub(crate) framework: Option, + /// Set together with `framework` (from `FrameworkBundleOptions`); names + /// the two virtual manifest modules synthesized when + /// `framework.server_components` is configured. + pub(crate) server_component_manifests: Option, pub graph: Graph<'a>, // `LinkerContext<'a>` borrows the same arena lifetime as `transpiler`. pub linker: LinkerContext<'a>, @@ -113,7 +120,7 @@ pub struct BundleV2<'a> { pub(crate) free_list: Vec>, /// See the comment in `Chunk.OutputPiece`. - pub(crate) unique_key: u64, + pub unique_key: u64, pub(crate) dynamic_import_entry_points: ArrayHashMap, pub(crate) finalizers: Vec, @@ -149,8 +156,60 @@ bun_core::declare_scope!(scan_counter, visible); /// dedups by path during a single `on_parse_task_complete` pass. pub(crate) type ResolveQueue = StringHashMap<*mut ParseTask>; -pub struct BakeOptions<'a> { - pub framework: bake::Framework, +/// Descriptor for one synthesized virtual module. The bundler creates the two +/// server-components manifest modules from these when +/// `Framework.server_components` is configured; the names are supplied by the +/// caller through `FrameworkBundleOptions.server_component_manifests`, so the +/// bundler hardcodes no framework-specific module names. +#[derive(Clone, Copy)] +pub struct VirtualModule { + /// Import specifier framework/user code writes (matched against + /// `import_record.path.text`). + pub specifier: &'static [u8], + /// Stable internal path (chunk naming / sourcemaps). + pub path: &'static [u8], + /// Path namespace, e.g. `bun`. + pub namespace: &'static [u8], +} + +impl VirtualModule { + /// Materialize the `Source` for this virtual module at the bundler's + /// reserved source `index`. + /// + /// `bun_paths::fs::Path<'static>` is the local TYPE_ONLY stub and does not + /// expose a built-in-path constructor, so the path is built field-by-field. + pub(crate) fn to_source(self, index: bun_ast::Index) -> bun_ast::Source { + bun_ast::Source { + path: bun_paths::fs::Path { + pretty: self.specifier, + text: self.path, + namespace: self.namespace, + is_disabled: false, + is_symlink: true, + }, + index, + ..Default::default() + } + } +} + +/// The two server-components manifest modules, at the bundler's fixed reserved +/// source indexes (`Index::BAKE_SERVER_DATA` / `Index::BAKE_CLIENT_DATA`). +#[derive(Clone, Copy)] +pub struct ServerComponentsManifests { + pub server: VirtualModule, + pub client: VirtualModule, +} + +/// Host-supplied configuration for a framework (multi-graph / server +/// components) bundle: the per-graph transpilers, the framework's bundler +/// view, and the manifest virtual-module names. +pub struct FrameworkBundleOptions<'a> { + pub framework: options::Framework, + /// Names of the two virtual manifest modules the bundler synthesizes when + /// `framework.server_components` is configured. Supplied by the caller so + /// the bundler hardcodes no framework-specific module specifiers. + pub server_component_manifests: ServerComponentsManifests, pub client_transpiler: NonNull>, pub ssr_transpiler: NonNull>, pub plugins: Option>, @@ -187,16 +246,16 @@ impl<'a> BundleV2<'a> { } /// Safe projection of the `client_transpiler` backref. Set once in `init` - /// (from `BakeOptions` or `initialize_client_transpiler`); the pointee is - /// live for `'a`. + /// (from `FrameworkBundleOptions` or `initialize_client_transpiler`); the + /// pointee is live for `'a`. #[inline] pub(crate) fn client_transpiler_ref(&self) -> Option<&Transpiler<'a>> { self.client_transpiler.as_deref() } /// Safe projection of the `plugins` backref (opaque C++ `BunPlugin`). - /// Set once in `init` from `BakeOptions` / completion config; live for the - /// bundle pass. + /// Set once in `init` from `FrameworkBundleOptions` / completion config; + /// live for the bundle pass. #[inline] pub(crate) fn plugins_ref(&self) -> Option<&JSBundlerPlugin> { // SAFETY: BACKREF — opaque C++ object owned by the completion task / @@ -237,12 +296,12 @@ impl<'a> BundleV2<'a> { pub(crate) fn transpiler_for_target(&mut self, target: options::Target) -> &mut Transpiler<'a> { // SAFETY: all three pointers are live for `'a` (set in `init`); the - // `client_transpiler` arm is only reached when bake populated it. - // Outside of server-components / dev-server, + // `client_transpiler` arm is only reached when the framework options + // populated it. Outside of server-components / dev-server, // the only case that doesn't return the main transpiler is a // browser-target request from a server-side build, which lazily // spins up a client transpiler. - if !self.transpiler.options.server_components && self.linker.dev_server.is_none() { + if !self.transpiler.options.server_components && !self.linker.has_dev_server { if target == Target::Browser && self.transpiler.options.target.is_server_side() { if let Some(p) = self.client_transpiler { // SAFETY: client_transpiler is live for `'a` (set in `init`); @@ -258,7 +317,8 @@ impl<'a> BundleV2<'a> { return &mut *self.transpiler; } // SAFETY: all three pointers are live for `'a` (set in `init`); the - // `client_transpiler` arm is only reached when bake populated it. + // `client_transpiler` arm is only reached when the framework options + // populated it. unsafe { match target { Target::Browser => self.client_transpiler.unwrap().assume_mut(), @@ -328,13 +388,11 @@ pub mod bv2_impl { use crate::Index; use crate::JSAst; use crate::bun_fs as Fs; - use crate::options_impl::TargetExt; use crate::transpiler::Transpiler; use crate::{bun_css, import_record}; use bun_alloc::{AllocError, Arena as ThreadLocalArena}; - use self::bake_types as bake; use crate::Error; use bun_ast::server_component_boundary; use bun_ast::{Binding, E, Expr, G, S}; @@ -347,336 +405,8 @@ pub mod bv2_impl { use bun_resolver::{self as _resolver, is_package_path}; use bun_threading::ThreadPool as ThreadPoolLib; - /// CYCLEBREAK(b0) TYPE_ONLY: pure value types from bake that bundler needs without - /// depending on the full DevServer. Move-in pass keeps these as the canonical defs; - /// bun_bake (post tier-6 collapse: bun_runtime::bake) re-exports from here. - pub mod bake_types { - #[repr(u8)] - #[derive(Copy, Clone, Eq, PartialEq, Debug, core::marker::ConstParamTy)] - pub enum Side { - Client = 0, - Server = 1, - } - #[repr(u8)] - #[derive(Copy, Clone, Eq, PartialEq, Debug)] - pub enum Graph { - Client = 0, - Server = 1, - Ssr = 2, - } - /// Used for the per-file `// path (target)` comment - /// in postProcessJSChunk and friends. - impl From for &'static str { - fn from(g: Graph) -> Self { - match g { - Graph::Client => "client", - Graph::Server => "server", - Graph::Ssr => "ssr", - } - } - } - impl Side { - pub fn graph(self) -> Graph { - match self { - Side::Client => Graph::Client, - Side::Server => Graph::Server, - } - } - } - /// The type of `CacheEntry.kind`. - #[repr(u8)] - #[derive(Copy, Clone, Eq, PartialEq, Debug)] - pub enum CacheKind { - Unknown = 0, - Js = 1, - Asset = 2, - Css = 3, - } - #[derive(Copy, Clone)] - pub struct CacheEntry { - pub kind: CacheKind, - } - /// INTERNAL_PREFIX ++ "/asset" = "/_bun/asset". - pub(crate) const ASSET_PREFIX: &str = "/_bun/asset"; - - /// TYPE_ONLY moved - /// down to bundler (T5); bake (in runtime, T6) constructs values of this type. - pub enum BuiltInModule { - Import(Box<[u8]>), - Code(Box<[u8]>), - } - - /// `EntryPointList` flags. - #[repr(transparent)] - #[derive(Copy, Clone, Default, Eq, PartialEq)] - pub struct EntryPointFlags(pub u8); - impl EntryPointFlags { - pub(crate) const CLIENT: u8 = 1 << 0; - pub(crate) const SERVER: u8 = 1 << 1; - pub(crate) const SSR: u8 = 1 << 2; - /// When set, `.CLIENT` is also set. - pub(crate) const CSS: u8 = 1 << 3; - #[inline] - pub(crate) fn client(self) -> bool { - self.0 & Self::CLIENT != 0 - } - #[inline] - pub(crate) fn server(self) -> bool { - self.0 & Self::SERVER != 0 - } - #[inline] - pub(crate) fn ssr(self) -> bool { - self.0 & Self::SSR != 0 - } - #[inline] - pub(crate) fn css(self) -> bool { - self.0 & Self::CSS != 0 - } - } - - /// TYPE_ONLY moved down; bundler - /// reads `.set` (count/keys/values) in `enqueue_entry_points_dev_server`. - #[derive(Default)] - pub struct EntryPointList { - pub set: bun_collections::StringArrayHashMap, - } - impl EntryPointList { - pub fn empty() -> Self { - Self { - set: bun_collections::StringArrayHashMap::new(), - } - } - } - - /// TYPE_ONLY subset of the `Framework` fields - /// the bundler/parser actually consult; `file_system_router_types` - /// stays in T6 because only `bake::FrameworkRouter` reads it. - #[non_exhaustive] - pub struct Framework { - pub(crate) built_in_modules: bun_collections::StringArrayHashMap, - /// Mirrors `Framework.server_components`. - pub(crate) server_components: Option, - /// Mirrors `Framework.react_fast_refresh` — read by the parser - /// (`js_parser/ast/Parser.rs:1997` resolves `framework.react_fast_refresh - /// .import_source`) when `features.react_fast_refresh` is on. - pub(crate) react_fast_refresh: Option, - /// Mirrors `Framework.is_built_in_react` — read by - /// `linker_context::generateChunksInParallel` to gate `BakeExtra`. - pub(crate) is_built_in_react: bool, - } - impl Framework { - /// Construct the bundler-side TYPE_ONLY view. Called from - /// `bun_runtime::bake::Framework::init_transpiler_with_options`; the - /// runtime owns the canonical `bake.Framework` and projects the - /// fields the bundler reads. - pub fn new( - built_in_modules: bun_collections::StringArrayHashMap, - server_components: Option, - react_fast_refresh: Option, - is_built_in_react: bool, - ) -> Self { - Self { - built_in_modules, - server_components, - react_fast_refresh, - is_built_in_react, - } - } - } - /// `Framework.ServerComponents` — full string - /// surface so the parser-side projection (ParseTask.rs `run_with_source_code`) - /// can forward user-configured `serverRegisterServerReference` / - /// `clientRegisterServerReference` instead of hardcoding defaults. - #[derive(Default, Clone)] - pub struct ServerComponents { - pub separate_ssr_graph: bool, - pub server_runtime_import: Box<[u8]>, - pub server_register_client_reference: Box<[u8]>, - pub server_register_server_reference: Box<[u8]>, - pub client_register_server_reference: Box<[u8]>, - } - #[derive(Clone)] - pub struct ReactFastRefresh { - pub import_source: Box<[u8]>, - } - - /// TYPE_ONLY moved down so the - /// linker can splice the runtime preamble without depending on bun_bake. - #[derive(Clone, Copy)] - pub struct HmrRuntime { - pub(crate) code: &'static [u8], - } - impl HmrRuntime { - pub(crate) const fn init(code: &'static [u8]) -> Self { - Self { code } - } - } - /// Alias used at the crate root (`crate::HmrRuntimeSide`); identical to `Side`. - pub(crate) type HmrRuntimeSide = Side; - - /// MOVE_DOWN bake→bundler: - /// the codegen'd `bake.client.js` / `bake.server.js` are loaded via - /// `bun_core::runtime_embed_file!` (same per-site `OnceLock` cache - /// `js_parser/runtime.rs` uses for `runtime.out.js`), so the storage lives - /// HERE — no upward link to `bun_runtime`. `bun_runtime::bake` keeps its - /// own `&'static ZStr` flavour for JSC/C++ handoff; this bundler-side copy - /// only needs `&[u8]` for the chunk preamble + sourcemap line skip, so the - /// NUL-termination dance is unnecessary. Per-side `OnceLock` - /// memoizes the `\n` count (`runtime_embed_file!` already caches the file - /// load, this caches the `init` scan so repeat calls are a `Copy`). - pub(crate) fn get_hmr_runtime(side: Side) -> HmrRuntime { - static CLIENT: std::sync::OnceLock = std::sync::OnceLock::new(); - static SERVER: std::sync::OnceLock = std::sync::OnceLock::new(); - match side { - Side::Client => *CLIENT.get_or_init(|| { - HmrRuntime::init( - bun_core::runtime_embed_file!(CodegenEager, "bake.client.js").as_bytes(), - ) - }), - // Server runtime is loaded once; non-eager. - Side::Server => *SERVER.get_or_init(|| { - HmrRuntime::init( - bun_core::runtime_embed_file!(Codegen, "bake.server.js").as_bytes(), - ) - }), - } - } + use crate::CacheKind; - /// `bun_ast::Source` is not `const`-constructible (owns a `fs::Path`), so these - /// are lazy statics. - pub(crate) static SERVER_VIRTUAL_SOURCE: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - // Inlined because `bun_paths::fs::Path<'static>` is the local TYPE_ONLY stub and - // does not expose a built-in-path constructor. - bun_ast::Source { - path: bun_paths::fs::Path { - pretty: b"bun:bake/server", - text: b"_bun/bake/server", - namespace: b"bun", - is_disabled: false, - is_symlink: true, - }, - index: bun_ast::Index(crate::Index::BAKE_SERVER_DATA.get()), - ..Default::default() - } - }); - pub(crate) static CLIENT_VIRTUAL_SOURCE: std::sync::LazyLock = - std::sync::LazyLock::new(|| bun_ast::Source { - path: bun_paths::fs::Path { - pretty: b"bun:bake/client", - text: b"_bun/bake/client", - namespace: b"bun", - is_disabled: false, - is_symlink: true, - }, - index: bun_ast::Index(crate::Index::BAKE_CLIENT_DATA.get()), - ..Default::default() - }); - - /// `EntryPointMap`. - /// Lives in the bundler (lower tier) so both `bun_runtime::bake::production` - /// and `BundleV2::generate_from_bake_production_cli` share ONE nominal type - /// (PORTING.md §Layering). Router-integration methods (`InsertionHandler`) - /// are added by `bun_runtime::bake` via a local trait impl. - pub mod production { - use super::Side; - - /// `OpaqueFileId` is the insertion index into `EntryPointMap.files`. - /// This is the same newtype as `framework_router::OpaqueFileId`; the - /// bake crate re-exports that one and converts via `.get()` only at - /// the FFI boundary. - #[repr(transparent)] - #[derive(Copy, Clone, Eq, PartialEq, Hash)] - pub struct OpaqueFileId(pub(crate) u32); - impl OpaqueFileId { - #[inline] - pub(crate) const fn init(i: u32) -> Self { - Self(i) - } - #[inline] - pub const fn get(self) -> u32 { - self.0 - } - } - - /// `EntryPointMap.InputFile`. The `Hash`/`Eq` impls below are content-based - /// (not byte-layout) — store a - /// `RawSlice` and let `bun_ptr` encapsulate the unsafe re-borrow. - /// `RawSlice: Send + Sync`, so no manual auto-trait impls are needed. - #[derive(Copy, Clone)] - pub struct InputFile { - abs_path: bun_ptr::RawSlice, - pub(crate) side: Side, - } - impl InputFile { - #[inline] - pub(crate) fn init(abs_path: &[u8], side: Side) -> Self { - Self { - abs_path: bun_ptr::RawSlice::new(abs_path), - side, - } - } - #[inline] - pub fn abs_path(&self) -> &[u8] { - // Backing allocation is owned by `EntryPointMap.owned_paths` - // (duped on insert) and outlives every key stored in `files`. - self.abs_path.slice() - } - } - impl core::hash::Hash for InputFile { - fn hash(&self, state: &mut H) { - state.write(self.abs_path()); - state.write_u8(self.side as u8); - } - } - impl PartialEq for InputFile { - fn eq(&self, other: &Self) -> bool { - self.side == other.side && self.abs_path() == other.abs_path() - } - } - impl Eq for InputFile {} - - /// Value side is `OutputFile.Index` — left as a placeholder until the - /// bundle is indexed; the bundler never reads it. - pub use crate::output_file::Index as OutputFileIndex; - - pub type EntryPointHashMap = bun_collections::ArrayHashMap; - - #[derive(Default)] - pub struct EntryPointMap { - pub root: Box<[u8]>, - /// `OpaqueFileId` is the insertion index into this map. - pub files: EntryPointHashMap, - /// Owned backing storage for the duped path bytes that `InputFile` - /// keys point into (raw ptr+len) — kept here so the allocations - /// drop with the map (no `Box::leak`). - pub owned_paths: Vec>, - } - impl EntryPointMap { - /// Mirrors `getOrPutEntryPoint`. Dupes `abs_path` on first insert - /// (owned by `owned_paths`; `Box` heap address is stable across the - /// move so the raw key pointer stays valid). - pub fn get_or_put_entry_point( - &mut self, - abs_path: &[u8], - side: Side, - ) -> crate::Result { - let probe = InputFile::init(abs_path, side); - if let Some(index) = self.files.get_index(&probe) { - return Ok(OpaqueFileId::init(index as u32)); - } - let owned: Box<[u8]> = Box::<[u8]>::from(abs_path); - let key = InputFile::init(&owned, side); - self.owned_paths.push(owned); - let index = self.files.count(); - // Value is the post-bundle output index; left as a placeholder until - // the bundle is indexed. - self.files.put_no_clobber(key, OutputFileIndex::init(0))?; - Ok(OpaqueFileId::init(index as u32)) - } - } - } - } use self::api as jsc_api; /// CYCLEBREAK(b0) TYPE_ONLY: data-only halves of `jsc::api::JSBundler` and @@ -691,7 +421,6 @@ pub mod bv2_impl { pub mod JSBundler { use super::super::BundleV2; use crate::options::{Loader, Target}; - use crate::options_impl::TargetExt; use crate::parse_task::ParseTask; use bun_ast::ImportKind; use bun_core::String as BunString; @@ -1253,10 +982,6 @@ pub mod bv2_impl { // backref liveness established by the `BackRef` invariant. unsafe { self.parse_task.get_mut() } } - #[inline] - pub(crate) fn bake_graph(&self) -> crate::bake_types::Graph { - self.parse_task().known_target.bake_graph() - } /// Hops to the JS thread to call the `onLoad` plugin chain — /// unless the pass is already cancelled: see `Resolve::dispatch`. pub(crate) fn dispatch(&mut self) { @@ -1279,7 +1004,7 @@ pub mod bv2_impl { } } pub fn run_on_js_thread(&mut self) { - let is_server_side = self.bake_graph() != crate::bake_types::Graph::Client; + let is_server_side = self.parse_task().known_target.is_server_side(); let default_loader = self.default_loader; // reshaped for borrowck — capture the erased self // pointer before borrowing fields immutably for the FFI call. @@ -1516,10 +1241,11 @@ pub mod bv2_impl { } // Unified with the canonical definitions at the parent module level (this - // avoids two distinct nominal `BundleV2`/`PendingImport`/`BakeOptions` types - // that previously caused widespread "expected `BundleV2`, found `BundleV2`" - // errors in cross-module call sites). - pub use super::{BakeOptions, BundleV2, PendingImport}; + // avoids two distinct nominal `BundleV2`/`PendingImport`/ + // `FrameworkBundleOptions` types that previously caused widespread + // "expected `BundleV2`, found `BundleV2`" errors in cross-module call + // sites). + pub use super::{BundleV2, FrameworkBundleOptions, PendingImport}; impl<'a> BundleV2<'a> { /// Folds the JS-loop lookup + enqueue so the bundler never dereferences @@ -1657,12 +1383,12 @@ pub mod bv2_impl { pub fn log_for_resolution_failures( &mut self, abs_path: &[u8], - bake_graph: bake::Graph, + target: options::Target, ) -> &mut bun_ast::Log { if let Some(dev) = self.dev_server_handle() { // CYCLEBREAK GENUINE: DevServer → vtable. // SAFETY: owner is a live *mut DevServer per handle invariant. - return unsafe { &mut *dev.log_for_resolution_failures(abs_path, bake_graph) }; + return unsafe { &mut *dev.log_for_resolution_failures(abs_path, target) }; } // SAFETY: `transpiler.log` is set from a live `*mut Log` in `init` and // outlives `BundleV2`. @@ -1905,7 +1631,7 @@ pub mod bv2_impl { } impl<'a> BundleV2<'a> { - pub(crate) fn find_reachable_files(&mut self) -> Result, Error> { + pub fn find_reachable_files(&mut self) -> Result, Error> { // RAII guard — `Ctx` ends the span on Drop. let _trace = crate::perf::trace("Bundler.findReachableFiles"); @@ -2133,7 +1859,7 @@ pub mod bv2_impl { } } - pub(crate) fn wait_for_parse(&mut self) { + pub fn wait_for_parse(&mut self) { // `tick_raw` (not `tick`) — `is_done` reborrows `*ctx` as // `&mut BundleV2`, and `BundleV2` (via `linker.r#loop`) owns the // `AnyEventLoop` slot, so holding `&mut AnyEventLoop` across the @@ -2167,7 +1893,7 @@ pub mod bv2_impl { self.graph.pool().worker_pool().dump_stats(label); } - pub(crate) fn scan_for_secondary_paths(&mut self) { + pub fn scan_for_secondary_paths(&mut self) { if !self.graph.has_any_secondary_paths { // Assert the boolean is accurate. #[cfg(debug_assertions)] @@ -2348,11 +2074,11 @@ pub mod bv2_impl { } } - // Tell Bake's Dev Server to wait for the file to be imported. + // Tell the dev server to wait for the file to be imported. dev.track_resolution_failure( &import_record.source_file, &import_record.specifier, - target.bake_graph(), + target, self.graph.input_files.items_loader() [import_record.importer_source_index as usize], ) @@ -2374,12 +2100,13 @@ pub mod bv2_impl { // `*self.transpiler.log` (both raw-pointer-derived), so detach the lifetime // so `self.graph.*` / `self.transpiler.*` reads below type-check. // SAFETY: log lives in DevServer / transpiler, disjoint from `self.graph`. - let log: &mut bun_ast::Log = unsafe { - bun_ptr::detach_lifetime_mut(self.log_for_resolution_failures( - &import_record.source_file, - target.bake_graph(), - )) - }; + let log: &mut bun_ast::Log = + unsafe { + bun_ptr::detach_lifetime_mut(self.log_for_resolution_failures( + &import_record.source_file, + target, + )) + }; { let record: &mut ImportRecord = @@ -2725,7 +2452,7 @@ pub mod bv2_impl { path.assert_pretty_is_valid(); // intern via `dupe_alloc` BEFORE writing back into `result` / // the path-to-source-index map. The dev-server path builds a fresh - // `bake_types::EntryPointList` with `Box<[u8]>` keys (DevServer.rs:3027) + // `EntryPointList` with `Box<[u8]>` keys // that drops as soon as `enqueue_entry_points_dev_server` returns; // `resolve_with_framework` then lifetime-erases that key into the // returned `Path`, so without interning here `ParseTask.path.text` (and @@ -2803,7 +2530,7 @@ pub mod bv2_impl { /// `heap` is not freed when `deinit`ing the BundleV2 pub fn init( transpiler: &'a mut Transpiler<'a>, - bake_options: Option>, + framework_options: Option>, _alloc: &bun_alloc::Arena, event_loop: EventLoop, cli_watch_flag: bool, @@ -2821,8 +2548,8 @@ pub mod bv2_impl { transpiler.options.target.is_bun() || transpiler.options.target == Target::Node; // SAFETY: `ssr_transpiler` intentionally aliases `transpiler` via a - // raw `*mut` until bake installs a separate SSR transpiler; all - // derefs go through the centralized accessors. + // raw `*mut` until the framework options install a separate SSR + // transpiler; all derefs go through the centralized accessors. let ssr_alias: *mut Transpiler<'a> = std::ptr::from_mut(transpiler); let mut this = Box::new(BundleV2 { transpiler, @@ -2830,6 +2557,7 @@ pub mod bv2_impl { owned_client_transpiler: None, ssr_transpiler: ssr_alias, framework: None, + server_component_manifests: None, graph: Graph { pool: bun_ptr::BackRef::dangling(), // set below heap, @@ -2862,7 +2590,7 @@ pub mod bv2_impl { has_any_top_level_await_modules: false, requested_exports: Vec::new(), }); - if let Some(bo) = bake_options { + if let Some(bo) = framework_options { // SAFETY: `bo.client_transpiler` is the caller's live, write-capable // transpiler pointer; it outlives this BundleV2. this.client_transpiler = Some(unsafe { @@ -2876,7 +2604,14 @@ pub mod bv2_impl { .map(|sc| sc.separate_ssr_graph) .unwrap_or(false); this.framework = Some(bo.framework); - this.linker.framework = this.framework.as_ref().map(bun_ptr::BackRef::new); + this.server_component_manifests = Some(bo.server_component_manifests); + this.linker.framework = + this.framework + .as_ref() + .map(|fw| crate::linker_context_mod::FrameworkInfo { + has_server_components: fw.server_components.is_some(), + is_built_in_react: fw.is_built_in_react, + }); this.plugins = bo.plugins; if this.transpiler.options.server_components { debug_assert!( @@ -2955,7 +2690,7 @@ pub mod bv2_impl { this.linker.options.metafile_markdown_path = unsafe { interned_slice(&this.transpiler.options.metafile_markdown_path) }; - this.linker.dev_server = this.dev_server; + this.linker.has_dev_server = this.dev_server.is_some(); let tp = ThreadPool::init(&*this, thread_pool)?; // errdefer this.graph.heap.deinit() — Drop handles arena teardown. @@ -3023,7 +2758,7 @@ pub mod bv2_impl { let dev = self .dev_server .unwrap_or_else(|| panic!("No dev server attached in asynchronous bundle job")); - self.finish_from_bake_dev_server(&dev).expect("oom"); + self.finish_from_dev_server(&dev).expect("oom"); } } @@ -3044,7 +2779,7 @@ pub mod bv2_impl { ) -> Result<(), Error> { self.enqueue_entry_points_common()?; // (variant != .dev_server) - self.reserve_source_indexes_for_bake()?; + self.reserve_source_indexes_for_server_components()?; // Setup entry points let num_entry_points = data.len(); @@ -3102,7 +2837,7 @@ pub mod bv2_impl { pub(crate) fn enqueue_entry_points_dev_server( &mut self, - files: &bake_types::EntryPointList, + files: &EntryPointList, css_data: &mut ArrayHashMap, ) -> Result<(), Error> { self.enqueue_entry_points_common()?; @@ -3172,9 +2907,9 @@ pub mod bv2_impl { dev.handle_parse_task_failure( err, if flags.client() { - bake::Graph::Client + Target::Browser } else { - bake::Graph::Server + server_target }, abs_path, // SAFETY: `transpiler` points at one of self's transpilers, live for `'a`. @@ -3220,26 +2955,24 @@ pub mod bv2_impl { Ok(()) } - pub(crate) fn enqueue_entry_points_bake_production( + /// Enqueue a fixed list of entry points, each with an explicit + /// per-entry target graph. The framework production build drives this + /// (each route file is enqueued for the graph the caller chose); + /// resolution always goes through the main transpiler. + pub fn enqueue_entry_points_with_targets( &mut self, - data: &bake_types::production::EntryPointMap, + entry_points: &[(&[u8], options::Target)], ) -> Result<(), Error> { self.enqueue_entry_points_common()?; - self.reserve_source_indexes_for_bake()?; + self.reserve_source_indexes_for_server_components()?; - let num_entry_points = data.files.count(); + let num_entry_points = entry_points.len(); self.graph.entry_points.reserve(num_entry_points); self.graph .input_files .ensure_unused_capacity(num_entry_points)?; - for key in data.files.keys() { - let abs_path = key.abs_path(); - let target = match key.side { - bake::Side::Client => Target::Browser, - bake::Side::Server => self.transpiler.options.target, - }; - + for &(abs_path, target) in entry_points { if self.enqueue_entry_point_on_resolve_plugin_if_needed(abs_path, target) { continue; } @@ -3293,7 +3026,10 @@ pub mod bv2_impl { Ok(()) } - fn clone_ast(&mut self) -> Result<(), Error> { + /// Clone the parse graph's AST into the linker graph and transfer + /// worker-allocator-owned AST pieces to the graph heap. Runs between + /// parsing and `linker.link` on every generate path. + pub fn clone_ast(&mut self) -> Result<(), Error> { let _trace = crate::perf::trace("Bundler.cloneAST"); self.linker.graph.ast = self.graph.ast.clone()?; @@ -3319,9 +3055,10 @@ pub mod bv2_impl { Ok(()) } - /// This generates the two asts for 'bun:bake/client' and 'bun:bake/server'. Both are generated - /// at the same time in one pass over the SCB list. - pub(crate) fn process_server_component_manifest_files(&mut self) -> Result<(), AllocError> { + /// This generates the asts for the two server-components manifest + /// virtual modules (named by `server_component_manifests`). Both are + /// generated at the same time in one pass over the SCB list. + pub fn process_server_component_manifest_files(&mut self) -> Result<(), AllocError> { // If a server components is not configured, do nothing let Some(fw) = &self.framework else { return Ok(()); @@ -3329,6 +3066,10 @@ pub mod bv2_impl { let Some(sc) = &fw.server_components else { return Ok(()); }; + // Set together with `framework` in `init` (both come from `FrameworkBundleOptions`). + let Some(manifests) = self.server_component_manifests else { + return Ok(()); + }; if !self.graph.kit_referenced_server_data && !self.graph.kit_referenced_client_data { return Ok(()); @@ -3341,8 +3082,10 @@ pub mod bv2_impl { unsafe { bun_ptr::detach_lifetime_ref::(self.arena()) }; let hmr = self.transpiler.options.hot_module_reloading; - let mut server = AstBuilder::init(alloc, &bake::SERVER_VIRTUAL_SOURCE, hmr)?; - let mut client = AstBuilder::init(alloc, &bake::CLIENT_VIRTUAL_SOURCE, hmr)?; + let server_source = manifests.server.to_source(Index::BAKE_SERVER_DATA); + let client_source = manifests.client.to_source(Index::BAKE_CLIENT_DATA); + let mut server = AstBuilder::init(alloc, &server_source, hmr)?; + let mut client = AstBuilder::init(alloc, &client_source, hmr)?; let mut server_manifest_props: Vec = Vec::new(); let mut client_manifest_props: Vec = Vec::new(); @@ -4058,87 +3801,7 @@ pub mod bv2_impl { Ok(this) } - pub fn generate_from_bake_production_cli( - entry_points: &bake_types::production::EntryPointMap, - server_transpiler: &'a mut Transpiler<'a>, - bake_options: BakeOptions<'a>, - alloc: &'a bun_alloc::Arena, - event_loop: EventLoop, - ) -> Result, Error> { - let mut this = BundleV2::init( - server_transpiler, - Some(bake_options), - alloc, - event_loop, - false, - None, - alloc, - )?; - this.unique_key = generate_unique_key(); - - // Wrap so every exit path hits the cleanup below; `chunks` must drop - // inside the closure, before `deinit_without_freeing_arena()`. - let result = (|| -> Result, Error> { - if this.transpiler.log().has_errors() { - return Err(crate::Error::BuildFailed); - } - - this.enqueue_entry_points_bake_production(entry_points)?; - - if this.transpiler.log().has_errors() { - return Err(crate::Error::BuildFailed); - } - - this.wait_for_parse(); - - if this.transpiler.log().has_errors() { - return Err(crate::Error::BuildFailed); - } - - this.scan_for_secondary_paths(); - - this.process_server_component_manifest_files()?; - - let reachable_files = this.find_reachable_files()?; - - this.process_files_to_copy(&reachable_files)?; - - this.add_server_component_boundaries_as_extra_entry_points()?; - - this.clone_ast()?; - - // SAFETY: see `generate_from_cli` — raw-ptr borrow sidestep for - // `link` takes a raw `*mut BundleV2` and only touches fields disjoint - // from `this.linker`. - let mut chunks = unsafe { - let bundle_ptr: *mut BundleV2 = &raw mut *this; - let ep = (*bundle_ptr).graph.entry_points.as_slice(); - // Value-copy (original preserved for `StaticRouteVisitor`). - // Borrow — do NOT `take` (see `generate_from_cli`). - let scbs = &(*bundle_ptr).graph.server_component_boundaries; - // Project `.linker` via `bundle_ptr` so no second `Box::deref_mut` - // retag invalidates `ep`/`scbs` (SB hygiene). - (*bundle_ptr) - .linker - .link(bundle_ptr, ep, scbs, &reachable_files)? - }; - - if chunks.is_empty() { - return Ok(Vec::new()); - } - - crate::linker_context_mod::generate_chunks_in_parallel::( - &mut this.linker, - &mut chunks, - ) - })(); - - this.deinit_without_freeing_arena(); - - result - } - - pub(crate) fn add_server_component_boundaries_as_extra_entry_points( + pub fn add_server_component_boundaries_as_extra_entry_points( &mut self, ) -> Result<(), Error> { // Prepare server component boundaries. Each boundary turns into two @@ -4168,10 +3831,7 @@ pub mod bv2_impl { Ok(()) } - pub(crate) fn process_files_to_copy( - &mut self, - reachable_files: &[Index], - ) -> Result<(), Error> { + pub fn process_files_to_copy(&mut self, reachable_files: &[Index]) -> Result<(), Error> { if self.graph.estimated_file_loader_count > 0 { // SAFETY: MultiArrayList columns are disjoint backing storage; raw-ptr // sidestep so we can hold several read-only column slices, one mutable @@ -4577,7 +4237,7 @@ pub mod bv2_impl { }; dev.handle_parse_task_failure( crate::Error::Plugin, - load.bake_graph(), + load.parse_task().known_target, source.path.key_for_incremental_graph(), &raw const temp_log, this, @@ -4691,7 +4351,7 @@ pub mod bv2_impl { let log: &mut bun_ast::Log = unsafe { bun_ptr::detach_lifetime_mut(this.log_for_resolution_failures( &resolve.import_record.source_file, - resolve.import_record.original_target.bake_graph(), + resolve.import_record.original_target, )) }; @@ -4906,7 +4566,7 @@ pub mod bv2_impl { jsc_api::JSBundler::ResolveValue::Err(err) => { let log = this.log_for_resolution_failures( &resolve.import_record.source_file, - resolve.import_record.original_target.bake_graph(), + resolve.import_record.original_target, ); let kind = err.kind; log.msgs.push(err.clone()); @@ -4995,7 +4655,7 @@ pub mod bv2_impl { // is invalidated ahead of `pool.workers_assignments` so no worker can // observe a half-torn-down transpiler. Clear the `client_transpiler` // alias first so it never dangles past the Box drop; in the - // `BakeOptions`-borrowed path `owned_client_transpiler` is `None` and + // `FrameworkBundleOptions`-borrowed path `owned_client_transpiler` is `None` and // the DevServer-owned pointer is left untouched. if let Some(ct) = self.owned_client_transpiler.as_deref_mut() { // `wire_after_move` boxed a higher-tier @@ -5237,9 +4897,9 @@ pub mod bv2_impl { } /// Dev Server uses this instead to run a subset of the transpiler, and to run it asynchronously. - pub fn start_from_bake_dev_server( + pub fn start_from_dev_server( &mut self, - bake_entry_points: &bake_types::EntryPointList, + entry_points: &EntryPointList, ) -> Result { self.unique_key = generate_unique_key(); @@ -5248,7 +4908,7 @@ pub mod bv2_impl { let mut ctx = DevServerInput { css_entry_points: ArrayHashMap::new(), }; - self.enqueue_entry_points_dev_server(bake_entry_points, &mut ctx.css_entry_points)?; + self.enqueue_entry_points_dev_server(entry_points, &mut ctx.css_entry_points)?; /* arena: help_catch_memory_issues — no-op (mimalloc TLH check) */ @@ -5259,7 +4919,7 @@ pub mod bv2_impl { // css_entry_points, etc.). After tier-6 collapse this fn should be hoisted into // bun_runtime::bake (which can name DevServer concretely) and call back into BundleV2 // helpers. Until then the entry-point fields are reached through the vtable. - pub(crate) fn finish_from_bake_dev_server( + pub fn finish_from_dev_server( &mut self, dev_server: &dispatch::DevServerHandle, ) -> Result<(), AllocError> { @@ -5343,7 +5003,7 @@ pub mod bv2_impl { dev_server .handle_parse_task_failure( crate::Error::InvalidCssImport, - bake::Graph::Client, + Target::Browser, sources[index].path.text, &raw const log, self, @@ -5779,13 +5439,17 @@ pub mod bv2_impl { Ok(out) } - fn reserve_source_indexes_for_bake(&mut self) -> Result<(), Error> { + fn reserve_source_indexes_for_server_components(&mut self) -> Result<(), Error> { let Some(fw) = &self.framework else { return Ok(()); }; if fw.server_components.is_none() { return Ok(()); } + // Set together with `framework` in `init` (both come from `FrameworkBundleOptions`). + let Some(manifests) = self.server_component_manifests else { + return Ok(()); + }; // Call this after debug_assert!(self.graph.input_files.len() == 1); @@ -5794,19 +5458,8 @@ pub mod bv2_impl { self.graph.ast.ensure_unused_capacity(2)?; self.graph.input_files.ensure_unused_capacity(2)?; - // The statics are `LazyLock` and `Source` is not `Clone`, so - // rebuild an owned `Source` from the static's clonable fields - // (`path`, `index`). - let server_source = bun_ast::Source { - path: bake::SERVER_VIRTUAL_SOURCE.path, - index: bake::SERVER_VIRTUAL_SOURCE.index, - ..Default::default() - }; - let client_source = bun_ast::Source { - path: bake::CLIENT_VIRTUAL_SOURCE.path, - index: bake::CLIENT_VIRTUAL_SOURCE.index, - ..Default::default() - }; + let server_source = manifests.server.to_source(Index::BAKE_SERVER_DATA); + let client_source = manifests.client.to_source(Index::BAKE_CLIENT_DATA); // OOM/capacity: fire-and-forget let _ = self.graph.input_files.append(crate::Graph::InputFile { @@ -5908,7 +5561,7 @@ pub mod bv2_impl { // parked on the graph row either: the dev server proceeds with // failed files and treats a populated `css` slot as a // successfully parsed CSS file (CSS entry point discovery and - // import ordering in `finish_from_bake_dev_server`), so a parked + // import ordering in `finish_from_dev_server`), so a parked // stylesheet would produce a CSS chunk for a failed file while // `graph.css_file_count` stays 0. if let Some(css_ref) = result.ast.css.take() { @@ -6008,15 +5661,17 @@ pub mod bv2_impl { continue; } - if let Some(fw) = &self.framework { + if let (Some(fw), Some(manifests)) = + (&self.framework, self.server_component_manifests) + { if fw.server_components.is_some() { let is_server = ctx.target.is_server_side(); - let src = if is_server { - &bake::SERVER_VIRTUAL_SOURCE + let (manifest, reserved_index) = if is_server { + (manifests.server, Index::BAKE_SERVER_DATA) } else { - &bake::CLIENT_VIRTUAL_SOURCE + (manifests.client, Index::BAKE_CLIENT_DATA) }; - if import_record.path.text == src.path.pretty { + if import_record.path.text == manifest.specifier { if self.dev_server.is_some() { import_record.flags.insert( bun_ast::ImportRecordFlags::IS_EXTERNAL_WITHOUT_SIDE_EFFECTS, @@ -6028,8 +5683,8 @@ pub mod bv2_impl { } else { self.graph.kit_referenced_client_data = true; } - import_record.path.namespace = b"bun"; - import_record.source_index = Index::source(src.index.0); + import_record.path.namespace = manifest.namespace; + import_record.source_index = Index::source(reserved_index.get()); } continue; } @@ -6113,52 +5768,45 @@ pub mod bv2_impl { // backrefs valid for `'a` (see `init`). Compute the raw ptr first, then // deref once, so the `&mut self` borrow doesn't span the rest of the loop // body. - let (transpiler_ptr, bake_graph, target): ( - *mut Transpiler<'a>, - bake::Graph, - options::Target, - ) = if import_record.tag == bun_ast::ImportRecordTag::BakeResolveToSsrGraph { - if self.framework.is_none() { - self.log_for_resolution_failures(source.path.text, bake::Graph::Ssr).add_error_fmt( - Some(source), - import_record.range.loc, - format_args!("The 'bunBakeGraph' import attribute cannot be used outside of a Bun Bake bundle"), - ); - continue; - } + let (transpiler_ptr, target): (*mut Transpiler<'a>, options::Target) = + if import_record.tag == bun_ast::ImportRecordTag::BakeResolveToSsrGraph { + if self.framework.is_none() { + self.log_for_resolution_failures(source.path.text, Target::ServerComponentsSsr).add_error_fmt( + Some(source), + import_record.range.loc, + format_args!("The 'bunBakeGraph' import attribute cannot be used outside of a Bun Bake bundle"), + ); + continue; + } - let is_supported = self.framework.as_ref().unwrap().server_components.is_some() - && self - .framework - .as_ref() - .unwrap() - .server_components - .as_ref() - .unwrap() - .separate_ssr_graph; - if !is_supported { - self.log_for_resolution_failures(source.path.text, bake::Graph::Ssr).add_error_fmt( - Some(source), - import_record.range.loc, - format_args!("Framework does not have a separate SSR graph to put this import into"), - ); - continue; - } + let is_supported = + self.framework.as_ref().unwrap().server_components.is_some() + && self + .framework + .as_ref() + .unwrap() + .server_components + .as_ref() + .unwrap() + .separate_ssr_graph; + if !is_supported { + self.log_for_resolution_failures(source.path.text, Target::ServerComponentsSsr).add_error_fmt( + Some(source), + import_record.range.loc, + format_args!("Framework does not have a separate SSR graph to put this import into"), + ); + continue; + } - ( - self.ssr_transpiler, - bake::Graph::Ssr, - Target::ServerComponentsSsr, - ) - } else { - ( - std::ptr::from_mut::>( - self.transpiler_for_target(ctx.target), - ), - ctx.target.bake_graph(), - ctx.target, - ) - }; + (self.ssr_transpiler, Target::ServerComponentsSsr) + } else { + ( + std::ptr::from_mut::>( + self.transpiler_for_target(ctx.target), + ), + ctx.target, + ) + }; // SAFETY: see note above — raw `*mut Transpiler` lives for `'a`. let transpiler: &mut Transpiler<'a> = unsafe { &mut *transpiler_ptr }; @@ -6249,7 +5897,7 @@ pub mod bv2_impl { // SAFETY: log lives in DevServer/transpiler, disjoint from `self.graph`. let log: &mut bun_ast::Log = unsafe { &mut *std::ptr::from_mut::( - self.log_for_resolution_failures(source.path.text, bake_graph), + self.log_for_resolution_failures(source.path.text, target), ) }; @@ -6277,7 +5925,7 @@ pub mod bv2_impl { dev.track_resolution_failure( source.path.text, import_record.path.text, - ctx.target.bake_graph(), // use the source file target not the altered one + ctx.target, // use the source file target not the altered one loader, ) .expect("oom"); @@ -6455,8 +6103,7 @@ pub mod bv2_impl { // blocks an assertion failure because the DevServer // reserves the HTML file's spot in IncrementalGraph for the // route definition. - let log = - self.log_for_resolution_failures(source.path.text, bake_graph); + let log = self.log_for_resolution_failures(source.path.text, target); log.add_range_error_fmt( Some(source), import_record.range, @@ -6472,15 +6119,14 @@ pub mod bv2_impl { import_record.source_index = Index::INVALID; - if let Some(entry) = dev_server.is_file_cached(path.text, bake_graph) { + if let Some(entry) = dev_server.is_file_cached(path.text, target) { let rel = bun_paths::resolve_path::relative_platform::< bun_paths::resolve_path::platform::Loose, false, >( self.transpiler.fs().top_level_dir, path.text ); - if loader == Loader::Html && entry.kind == bake_types::CacheKind::Asset - { + if loader == Loader::Html && entry.kind == CacheKind::Asset { // Overload `path.text` to point to the final URL // This information cannot be queried while printing because a lock wouldn't get held. let hash = dev_server @@ -6495,7 +6141,7 @@ pub mod bv2_impl { self.arena() .alloc_str(&format!( "{}/{:016x}{}", - bake_types::ASSET_PREFIX, + bun_options_types::DEV_SERVER_ASSET_PREFIX, hash, bstr::BStr::new(bun_paths::extension(path.text)), )) @@ -6511,9 +6157,7 @@ pub mod bv2_impl { .path_with_pretty_initialized(path, target) .expect("oom"), ); - if loader == Loader::Html - || entry.kind == bake_types::CacheKind::Css - { + if loader == Loader::Html || entry.kind == CacheKind::Css { import_record.path.is_disabled = true; } } @@ -7334,7 +6978,7 @@ pub mod bv2_impl { dev_server .handle_parse_task_failure( err.err, - err.target.bake_graph(), + err.target, abs_path, &raw const err.log, std::ptr::from_mut(this), @@ -7732,6 +7376,49 @@ pub mod bv2_impl { pub imported_on_server: bool, } + /// `EntryPointList` flags: which graph(s) a dev-server entry point is + /// bundled into. + #[repr(transparent)] + #[derive(Copy, Clone, Default, Eq, PartialEq)] + pub struct EntryPointFlags(pub u8); + impl EntryPointFlags { + pub(crate) const CLIENT: u8 = 1 << 0; + pub(crate) const SERVER: u8 = 1 << 1; + pub(crate) const SSR: u8 = 1 << 2; + /// When set, `.CLIENT` is also set. + pub(crate) const CSS: u8 = 1 << 3; + #[inline] + pub(crate) fn client(self) -> bool { + self.0 & Self::CLIENT != 0 + } + #[inline] + pub(crate) fn server(self) -> bool { + self.0 & Self::SERVER != 0 + } + #[inline] + pub(crate) fn ssr(self) -> bool { + self.0 & Self::SSR != 0 + } + #[inline] + pub(crate) fn css(self) -> bool { + self.0 & Self::CSS != 0 + } + } + + /// Entry points for a dev-server bundle pass, keyed by absolute path; the + /// caller builds it, `enqueue_entry_points_dev_server` reads `.set`. + #[derive(Default)] + pub struct EntryPointList { + pub set: bun_collections::StringArrayHashMap, + } + impl EntryPointList { + pub fn empty() -> Self { + Self { + set: bun_collections::StringArrayHashMap::new(), + } + } + } + /// The lifetime of this structure is tied to the bundler's arena pub struct DevServerInput { pub(crate) css_entry_points: ArrayHashMap, @@ -7744,7 +7431,7 @@ pub mod bv2_impl { pub html_files: ArrayHashMap, } - pub(crate) fn generate_unique_key() -> u64 { + pub fn generate_unique_key() -> u64 { let key = bun_core::fast_random() & 0x0FFFFFFF_FFFFFFFF_u64; // without this check, putting unique_key in an object key would // sometimes get converted to an identifier. ensuring it starts diff --git a/src/bundler/bundled_ast.rs b/src/bundler/bundled_ast.rs index f661a7706948..e0f6ba61fb55 100644 --- a/src/bundler/bundled_ast.rs +++ b/src/bundler/bundled_ast.rs @@ -53,7 +53,7 @@ pub struct BundledAst<'arena> { /// These are stored at the AST level instead of on individual AST nodes so /// they can be manipulated efficiently without a full AST traversal - pub(crate) import_records: import_record::List<'arena>, + pub import_records: import_record::List<'arena>, // Ast.hashbang is `StoreStr`; mirror it here so init/to_ast can // round-trip. @@ -68,8 +68,8 @@ pub struct BundledAst<'arena> { // Only meaningful when flags.HAS_CHAR_FREQ is set; zero-initialized otherwise. pub(crate) char_freq: CharFreq, pub(crate) exports_ref: Ref, - pub(crate) module_ref: Ref, - pub(crate) wrapper_ref: Ref, + pub module_ref: Ref, + pub wrapper_ref: Ref, pub(crate) require_ref: Ref, pub(crate) top_level_await_keyword: bun_ast::Range, pub tla_check: TlaCheck, diff --git a/src/bundler/lib.rs b/src/bundler/lib.rs index 20338efed2b8..cc52b20ec39d 100644 --- a/src/bundler/lib.rs +++ b/src/bundler/lib.rs @@ -126,9 +126,6 @@ pub mod linker_context { #[path = "convertStmtsForChunk.rs"] pub(crate) mod convert_stmts_for_chunk; - #[path = "convertStmtsForChunkForDevServer.rs"] - pub mod convert_stmts_for_chunk_for_dev_server; - #[path = "doStep5.rs"] pub mod do_step5; @@ -232,8 +229,9 @@ pub(crate) use bun_ast::{Index, IndexInt}; // Re-export the `options` module. `Loader`/`Target` live in // `bun_options_types::bundle_enums` — `options_impl` re-exports the canonical // defs, so there is exactly ONE nominal type for each across -// bundler/resolver/js_parser. Bundler-only behaviour hangs off -// `TargetExt`/`LoaderExt` extension traits in `options_impl`. +// bundler/resolver/js_parser. Bundler-only behaviour hangs off the +// `LoaderExt` extension trait in `options_impl` and `TargetExt` in +// `bake_types`. pub mod options { pub use super::OutputFile; pub use super::options_impl::*; @@ -288,10 +286,12 @@ pub mod options { } /// Which graph an output belongs to. - /// Re-export of the canonical def in `crate::bake_types` (bundle_v2.rs). + /// Re-export of the canonical def in `crate::bake_types`. pub use crate::bake_types::Side; - pub use crate::bake_types::Framework; + // `Framework` (the minimal bundler-side view of `bake::Framework`) is + // defined in `options_impl` and exposed by the glob above; `bake_types` + // re-exports it for the bake seam. } /// Re-export so `crate::RuntimeTranspilerCache` resolves for `transpiler::ParseOptions` @@ -301,12 +301,10 @@ pub mod options { pub use cache::RuntimeTranspilerCacheExt; // ────────────────────────────────────────────────────────────────────────── -// Re-export the canonical `bake_types` defs from -// `bundle_v2` so there is exactly ONE nominal `Side`/`Graph`/`Framework` etc. -// across the crate (the previous inline copy here diverged and produced -// "expected `bake_types::Graph`, found `bake_types::Graph`" errors). +// TYPE_ONLY seam module shared with `bun_runtime::bake` — the single nominal +// `Side`/`Graph`/`Framework` etc. across the crate. // ────────────────────────────────────────────────────────────────────────── -pub use bundle_v2::bake_types; +pub mod bake_types; // ────────────────────────────────────────────────────────────────────────── // Re-export the canonical `dispatch` module from @@ -317,17 +315,37 @@ pub use bundle_v2::dispatch; // ── link-interfaces (must be at crate root so `$crate::__alias` resolves) ── // Re-exported through `bundle_v2::dispatch` for existing call sites. +/// The type of `CacheEntry.kind`. Seam type of the `DevServerHandle` vtable +/// below (`is_file_cached` returns it); the implementing side translates its +/// own cache-entry kind into this. +#[repr(u8)] +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub enum CacheKind { + Unknown = 0, + Js = 1, + Asset = 2, + Css = 3, +} +/// What the dev server has cached for a path+target; returned through the +/// `DevServerHandle::is_file_cached` slot. +#[derive(Copy, Clone)] +pub struct CacheEntry { + pub kind: CacheKind, +} + // Erased handle to `bake::DevServer`. The struct stores a `&'a mut [Chunk]` -// it mutates through, hence `*mut`. +// it mutates through, hence `*mut`. Slots speak bundler vocabulary +// (`bun_ast::Target`, not the dev server's graph model); the implementing +// side maps targets onto its own graphs. bun_dispatch::link_interface! { pub DevServerHandle[Bake] { fn barrel_needed_exports() -> *mut bun_collections::StringArrayHashMap>; - fn log_for_resolution_failures(abs_path: &[u8], graph: bake_types::Graph) -> *mut bun_ast::Log; + fn log_for_resolution_failures(abs_path: &[u8], target: bun_ast::Target) -> *mut bun_ast::Log; fn finalize_bundle(bv2: *mut bundle_v2::BundleV2<'_>, result: *mut bundle_v2::DevServerOutput<'_>) -> Result<(), crate::Error>; - fn handle_parse_task_failure(err: crate::Error, graph: bake_types::Graph, abs_path: &[u8], log: *const bun_ast::Log, bv2: *mut bundle_v2::BundleV2<'_>) -> Result<(), crate::Error>; + fn handle_parse_task_failure(err: crate::Error, target: bun_ast::Target, abs_path: &[u8], log: *const bun_ast::Log, bv2: *mut bundle_v2::BundleV2<'_>) -> Result<(), crate::Error>; fn put_or_overwrite_asset(path: *const (), contents: &[u8], content_hash: u64) -> Result<(), crate::Error>; - fn track_resolution_failure(import_source: &[u8], specifier: &[u8], renderer: bake_types::Graph, loader: bun_ast::Loader) -> Result<(), crate::Error>; - fn is_file_cached(abs_path: &[u8], side: bake_types::Graph) -> Option; + fn track_resolution_failure(import_source: &[u8], specifier: &[u8], target: bun_ast::Target, loader: bun_ast::Loader) -> Result<(), crate::Error>; + fn is_file_cached(abs_path: &[u8], target: bun_ast::Target) -> Option; fn asset_hash(abs_path: &[u8]) -> Option; fn current_bundle_start_data() -> *mut (); fn register_barrel_with_deferrals(path: &[u8]) -> Result<(), crate::Error>; @@ -343,6 +361,39 @@ unsafe impl Send for DevServerHandle {} // SAFETY: see `Send` above — sharing the tagged pointer is sound for the same reason. unsafe impl Sync for DevServerHandle {} +/// Statement conversion for `options::Format::InternalBakeDev` output: emits +/// the packed HMR-module shape. The encoding is owned by the HMR runtime that +/// decodes it, so the body lives in `bun_runtime`'s bake module; the bundler +/// reaches it through the definer-prefixed extern hook below (same pattern as +/// the `__bun_jsc_*` hooks in `bundle_v2::dispatch`). `loaders`/`sources` are +/// the parse graph's input-file columns, computed once per part range by the +/// caller. +#[inline] +pub(crate) fn convert_stmts_for_chunk_hmr( + stmts: &mut linker_context_mod::StmtList, + part_stmts: &[bun_ast::Stmt], + bump: &bun_alloc::Arena, + ast: &mut BundledAst<'_>, + loaders: &[bun_ast::Loader], + sources: &[bun_ast::Source], +) -> Result<(), bun_alloc::AllocError> { + __bun_bake_convert_stmts_for_chunk_hmr(stmts, part_stmts, bump, ast, loaders, sources) +} + +unsafe extern "Rust" { + /// Defined `#[no_mangle]` in `bun_runtime` (`bake/hmr_module_format.rs`). + /// All arguments are safe Rust types (no raw-pointer preconditions), so + /// the link-time-resolved body upholds Rust's invariants on its own. + safe fn __bun_bake_convert_stmts_for_chunk_hmr( + stmts: &mut linker_context_mod::StmtList, + part_stmts: &[bun_ast::Stmt], + bump: &bun_alloc::Arena, + ast: &mut BundledAst<'_>, + loaders: &[bun_ast::Loader], + sources: &[bun_ast::Source], + ) -> Result<(), bun_alloc::AllocError>; +} + // VirtualMachine accessors for `normalize_specifier` / `get_loader_and_virtual_source`. // `bun_runtime::jsc_hooks` provides the `Runtime` arm. bun_dispatch::link_interface! { diff --git a/src/bundler/linker_context/README.md b/src/bundler/linker_context/README.md index 93bc03a3bb84..eaea2d73cc0f 100644 --- a/src/bundler/linker_context/README.md +++ b/src/bundler/linker_context/README.md @@ -1001,15 +1001,7 @@ var init_demo = __esm(() => { This function is essential for maintaining JavaScript module semantics across different output formats while enabling optimal bundling strategies. -#### `convertStmtsForChunkForDevServer.rs` - -**Purpose**: Special statement conversion for development server (HMR). - -**Key functions**: - -- HMR-specific code generation -- Development-time optimizations -- Live reload integration +The dev-server (HMR) variant of this conversion is not in this directory: it lives in `src/runtime/bake/hmr_module_format.rs` and is reached through the `__bun_bake_convert_stmts_for_chunk_hmr` link-time hook declared in `src/bundler/lib.rs`. ### Post-Processing Phase diff --git a/src/bundler/linker_context/computeChunks.rs b/src/bundler/linker_context/computeChunks.rs index a7d41a4e7aac..50ccdc96e6e7 100644 --- a/src/bundler/linker_context/computeChunks.rs +++ b/src/bundler/linker_context/computeChunks.rs @@ -38,7 +38,7 @@ pub(crate) fn compute_chunks( ) -> crate::Result> { let _trace = bun_core::perf::trace("Bundler.computeChunks"); - debug_assert!(this.dev_server.is_none()); // use + debug_assert!(!this.has_dev_server); // use let arena = Arena::new(); let temp = &arena; diff --git a/src/bundler/linker_context/generateChunksInParallel.rs b/src/bundler/linker_context/generateChunksInParallel.rs index 536354800a08..a43d48108596 100644 --- a/src/bundler/linker_context/generateChunksInParallel.rs +++ b/src/bundler/linker_context/generateChunksInParallel.rs @@ -43,7 +43,7 @@ use crate::linker_context_mod::debug; // Const generics cannot vary the return type, so we always return // `Vec` and the IS_DEV_SERVER path returns an empty Vec. -pub(crate) fn generate_chunks_in_parallel( +pub fn generate_chunks_in_parallel( c: &mut LinkerContext, chunks: &mut [Chunk], ) -> crate::Result> { @@ -263,7 +263,7 @@ pub(crate) fn generate_chunks_in_parallel( // A part that failed to print (e.g. the recursion guard tripped on a // deeply nested AST) must fail the build instead of joining the chunk // as silently truncated output. Dev server excluded: its callers turn - // any `Err` here into an OOM panic (see `finish_from_bake_dev_server`), + // any `Err` here into an OOM panic (see `finish_from_dev_server`), // so unprintable parts keep the old dropped-code behavior there. if !IS_DEV_SERVER { let mut had_print_error = false; @@ -1245,7 +1245,7 @@ pub(crate) fn generate_chunks_in_parallel( Some( chunk.entry_point.source_index() - (if let Some(fw) = c.framework { - if fw.server_components.is_some() { 3 } else { 1 } + if fw.has_server_components { 3 } else { 1 } } else { 1 }) as u32, diff --git a/src/bundler/linker_context/generateCodeForFileInChunkJS.rs b/src/bundler/linker_context/generateCodeForFileInChunkJS.rs index e41a1cb53ba8..a9d6d12cf1a4 100644 --- a/src/bundler/linker_context/generateCodeForFileInChunkJS.rs +++ b/src/bundler/linker_context/generateCodeForFileInChunkJS.rs @@ -19,7 +19,6 @@ use bun_ast::{B, Binding, E, Expr, G, Ref, S, Stmt}; use bun_js_parser::lexer as js_lexer; use super::convert_stmts_for_chunk::convert_stmts_for_chunk; -use super::convert_stmts_for_chunk_for_dev_server::convert_stmts_for_chunk_for_dev_server; #[allow(clippy::too_many_arguments)] pub fn generate_code_for_file_in_chunk_js<'r, 'src>( @@ -72,18 +71,22 @@ pub fn generate_code_for_file_in_chunk_js<'r, 'src>( if c.options.output_format == OutputFormat::InternalBakeDev { 'brk: { if part_range.source_index.is_runtime() { - debug_assert!(c.dev_server.is_none()); + debug_assert!(!c.has_dev_server); break 'brk; // this is from `bun build --format=internal_bake_dev` } let hmr_api_ref = ast.wrapper_ref; + let input_files = &c.parse_graph().input_files; + let loaders = input_files.items_loader(); + let sources = input_files.items_source(); + // SAFETY: see `parts` raw-pointer note above. for part in unsafe { (*parts).iter() } { let part_stmts: &[Stmt] = part.stmts.slice(); - if let Err(err) = - convert_stmts_for_chunk_for_dev_server(c, stmts, part_stmts, arena, &mut ast) - { + if let Err(err) = crate::convert_stmts_for_chunk_hmr( + stmts, part_stmts, arena, &mut ast, loaders, sources, + ) { return PrintResult::Err(err.into()); } } diff --git a/src/bundler/linker_context/generateCompileResultForHtmlChunk.rs b/src/bundler/linker_context/generateCompileResultForHtmlChunk.rs index e858c718a358..159619373045 100644 --- a/src/bundler/linker_context/generateCompileResultForHtmlChunk.rs +++ b/src/bundler/linker_context/generateCompileResultForHtmlChunk.rs @@ -157,7 +157,7 @@ impl<'a> HTMLProcessorHandler for HTMLLoader<'a> { return; } - if self.linker.dev_server.is_some() { + if self.linker.has_dev_server { if !unique_key_for_additional_files.is_empty() { set_attribute(element, url_attribute, unique_key_for_additional_files); } else if import_record.path.is_disabled @@ -307,7 +307,7 @@ impl<'a> HTMLLoader<'a> { fn end_head_tag_handler(opaque_this: *mut (), end: &mut EndTag<'_>) -> HandlerResult { // SAFETY: `opaque_this` is the erased `&mut HTMLLoader` from `register_end_tag_handler`. let this: &mut Self = unsafe { &mut *opaque_this.cast::() }; - if this.linker.dev_server.is_none() { + if !this.linker.has_dev_server { this.add_head_tags(end); } else { this.end_tag_indices.head = Some(u32::try_from(this.output.len()).expect("int cast")); @@ -318,7 +318,7 @@ impl<'a> HTMLLoader<'a> { fn end_body_tag_handler(opaque_this: *mut (), end: &mut EndTag<'_>) -> HandlerResult { // SAFETY: `opaque_this` is the erased `&mut HTMLLoader` from `register_end_tag_handler`. let this: &mut Self = unsafe { &mut *opaque_this.cast::() }; - if this.linker.dev_server.is_none() { + if !this.linker.has_dev_server { if this.compile_to_standalone_html { // In standalone mode, insert JS before so DOM is available this.add_body_tags(end); @@ -334,7 +334,7 @@ impl<'a> HTMLLoader<'a> { fn end_html_tag_handler(opaque_this: *mut (), end: &mut EndTag<'_>) -> HandlerResult { // SAFETY: `opaque_this` is the erased `&mut HTMLLoader` from `register_end_tag_handler`. let this: &mut Self = unsafe { &mut *opaque_this.cast::() }; - if this.linker.dev_server.is_none() { + if !this.linker.has_dev_server { if this.compile_to_standalone_html { // Fallback: if no was found, insert both CSS and JS before this.add_head_tags(end); @@ -371,7 +371,7 @@ fn generate_compile_result_for_html_chunk_impl<'a>( let log: *mut Log = c.log; let minify_whitespace = c.options.minify_whitespace; let compile_to_standalone_html = c.options.compile_mode.is_standalone_html(); - let has_dev_server = c.dev_server.is_some(); + let has_dev_server = c.has_dev_server; let contents: &[u8] = &sources[source_index as usize].contents; let records = import_records[source_index as usize].as_slice(); diff --git a/src/bundler/linker_context/generateCompileResultForJSChunk.rs b/src/bundler/linker_context/generateCompileResultForJSChunk.rs index 5219d91ef861..a33455bc136f 100644 --- a/src/bundler/linker_context/generateCompileResultForJSChunk.rs +++ b/src/bundler/linker_context/generateCompileResultForJSChunk.rs @@ -71,7 +71,6 @@ fn generate_compile_result_for_js_chunk_impl( // `BufferWriter::init()` output is allocated from the global heap and // outlives the task's CompileResult consumption, so a per-dev-server // arena would only be a perf optimization. - let _ = c.dev_server; // temporary_arena / stmt_list are initialized in Worker::create before any task runs. let arena = worker diff --git a/src/bundler/linker_context/postProcessJSChunk.rs b/src/bundler/linker_context/postProcessJSChunk.rs index f503f3254274..062a92074736 100644 --- a/src/bundler/linker_context/postProcessJSChunk.rs +++ b/src/bundler/linker_context/postProcessJSChunk.rs @@ -1,10 +1,10 @@ use crate::LinkerContext; use crate::analyze_transpiled_module::ModuleInfo; -use crate::bundle_v2::bake_types::{HmrRuntimeSide, get_hmr_runtime}; +use crate::bake_types::TargetExt as _; +use crate::bake_types::{HmrRuntimeSide, get_hmr_runtime}; use crate::linker_context_mod::{GenerateChunkCtx, LinkerOptionsMode}; use crate::mal_prelude::*; use crate::options; -use crate::options_impl::TargetExt as _; use crate::{ Chunk, CompileResult, CompileResultForSourceMap, Index, RefImportData, ResolvedExports, ThreadPool, @@ -451,7 +451,7 @@ pub(crate) fn post_process_js_chunk( let show_comments = c.options.mode == LinkerOptionsMode::Bundle && !c.options.minify_whitespace; let emit_targets_in_commands = - show_comments && c.framework.is_some_and(|fw| fw.server_components.is_some()); + show_comments && c.framework.is_some_and(|fw| fw.has_server_components); let sources: &[bun_ast::Source] = c.parse_graph().input_files.items_source(); let targets: &[options::Target] = c.parse_graph().ast.items_target(); diff --git a/src/bundler/linker_context/writeOutputFilesToDisk.rs b/src/bundler/linker_context/writeOutputFilesToDisk.rs index 4e3d991dde76..3ce71a484754 100644 --- a/src/bundler/linker_context/writeOutputFilesToDisk.rs +++ b/src/bundler/linker_context/writeOutputFilesToDisk.rs @@ -576,7 +576,7 @@ pub(crate) fn write_output_files_to_disk( 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()) { + let offset: u32 = if c.framework.is_some_and(|fw| fw.has_server_components) { 3 } else { 1 diff --git a/src/bundler/options.rs b/src/bundler/options.rs index 66e0c833cd85..e235b223a1da 100644 --- a/src/bundler/options.rs +++ b/src/bundler/options.rs @@ -250,78 +250,115 @@ const DEFAULT_MAIN_FIELDS_BUN: &[&[u8]] = &[ TARGET_MAIN_FIELD_NAMES[3], ]; -/// Bundler-only `Target` methods. Extension trait per PORTING.md crate-tier -/// rule — the canonical `Target` lives in `bun_options_types` (lower tier) and -/// cannot depend on `bake_types` / `StringHashMap`. Re-exported through -/// `bun_bundler::options` so `use bun_bundler::options::TargetExt;` makes -/// `.bake_graph()` etc. available on the single canonical type. -pub trait TargetExt: Copy { - // `fromJS` lives in `bun_bundler_jsc::options_jsc::target_from_js` - // (PORTING.md "*_jsc alias" rule). - - fn bake_graph(self) -> crate::bake_types::Graph; - fn out_extensions(self) -> StringHashMap<&'static [u8]>; - - // Original comment: - // The neutral target is for people that don't want esbuild to try to - // pick good defaults for their platform. In that case, the list of main - // fields is empty by default. You must explicitly configure it yourself. - // array.set(Target.neutral, &listc); - fn default_main_fields_map() -> EnumMap { - EnumMap::from_fn(|k| match k { - Target::Node => DEFAULT_MAIN_FIELDS_NODE, - Target::Browser => DEFAULT_MAIN_FIELDS_BROWSER, - Target::Bun => DEFAULT_MAIN_FIELDS_BUN, - Target::BunMacro => DEFAULT_MAIN_FIELDS_BUN, - Target::ServerComponentsSsr => DEFAULT_MAIN_FIELDS_BUN, - }) - } - - fn default_conditions_map() -> EnumMap { - EnumMap::from_fn(|k| match k { - Target::Node => &[b"node" as &[u8]][..], - Target::Browser => &[b"browser" as &[u8], b"module"][..], - Target::Bun => &[b"bun" as &[u8], b"node"][..], - Target::ServerComponentsSsr => &[b"bun" as &[u8], b"node"][..], - Target::BunMacro => &[b"macro" as &[u8], b"bun", b"node"][..], - }) - } +/// TYPE_ONLY subset of the framework fields the bundler/parser actually +/// consult — host-owned bundler vocabulary (`built_in_modules`, +/// `server_components`, `react_fast_refresh`, `is_built_in_react`). +/// `bake_types` (and through it `bun_runtime::bake`) re-exports this as the +/// one nominal type; the runtime projects its canonical `bake.Framework` +/// superset into this view via `as_bundler_view`. `file_system_router_types` +/// stays in the runtime because only `bake::FrameworkRouter` reads it. +#[non_exhaustive] +pub struct Framework { + pub(crate) built_in_modules: StringArrayHashMap, + /// Mirrors `Framework.server_components`. + pub(crate) server_components: Option, + /// Mirrors `Framework.react_fast_refresh` — read by the parser + /// (`js_parser/ast/Parser.rs:1997` resolves `framework.react_fast_refresh + /// .import_source`) when `features.react_fast_refresh` is on. + pub(crate) react_fast_refresh: Option, + /// Mirrors `Framework.is_built_in_react` — read by + /// `linker_context::generateChunksInParallel` to gate `BakeExtra`. + pub(crate) is_built_in_react: bool, } - -impl TargetExt for Target { - fn bake_graph(self) -> crate::bake_types::Graph { - match self { - Target::Browser => crate::bake_types::Graph::Client, - Target::ServerComponentsSsr => crate::bake_types::Graph::Ssr, - Target::BunMacro | Target::Bun | Target::Node => crate::bake_types::Graph::Server, +impl Framework { + /// Construct the bundler-side TYPE_ONLY view. Called from + /// `bun_runtime::bake::Framework::init_transpiler_with_options`; the + /// runtime owns the canonical `bake.Framework` and projects the + /// fields the bundler reads. + pub fn new( + built_in_modules: StringArrayHashMap, + server_components: Option, + react_fast_refresh: Option, + is_built_in_react: bool, + ) -> Self { + Self { + built_in_modules, + server_components, + react_fast_refresh, + is_built_in_react, } } +} +/// `Framework.ServerComponents` — full string +/// surface so the parser-side projection (ParseTask.rs `run_with_source_code`) +/// can forward user-configured `serverRegisterServerReference` / +/// `clientRegisterServerReference` instead of hardcoding defaults. +#[derive(Default, Clone)] +pub struct ServerComponents { + pub separate_ssr_graph: bool, + pub server_runtime_import: Box<[u8]>, + pub server_register_client_reference: Box<[u8]>, + pub server_register_server_reference: Box<[u8]>, + pub client_register_server_reference: Box<[u8]>, +} +#[derive(Clone)] +pub struct ReactFastRefresh { + pub import_source: Box<[u8]>, +} - fn out_extensions(self) -> StringHashMap<&'static [u8]> { - let mut exts = StringHashMap::<&'static [u8]>::default(); +// `Target::fromJS` lives in `bun_bundler_jsc::options_jsc::target_from_js` +// (PORTING.md "*_jsc alias" rule); `from_api`/`to_api` on +// `bun_options_types::TargetExt`. + +// Original comment: +// The neutral target is for people that don't want esbuild to try to +// pick good defaults for their platform. In that case, the list of main +// fields is empty by default. You must explicitly configure it yourself. +// array.set(Target.neutral, &listc); +fn default_main_fields_map() -> EnumMap { + EnumMap::from_fn(|k| match k { + Target::Node => DEFAULT_MAIN_FIELDS_NODE, + Target::Browser => DEFAULT_MAIN_FIELDS_BROWSER, + Target::Bun => DEFAULT_MAIN_FIELDS_BUN, + Target::BunMacro => DEFAULT_MAIN_FIELDS_BUN, + Target::ServerComponentsSsr => DEFAULT_MAIN_FIELDS_BUN, + }) +} - const OUT_EXTENSIONS_LIST: &[&[u8]] = &[ - b".js", b".cjs", b".mts", b".cts", b".ts", b".tsx", b".jsx", b".json", - ]; +fn default_conditions_map() -> EnumMap { + EnumMap::from_fn(|k| match k { + Target::Node => &[b"node" as &[u8]][..], + Target::Browser => &[b"browser" as &[u8], b"module"][..], + Target::Bun => &[b"bun" as &[u8], b"node"][..], + Target::ServerComponentsSsr => &[b"bun" as &[u8], b"node"][..], + Target::BunMacro => &[b"macro" as &[u8], b"bun", b"node"][..], + }) +} - if self == Target::Node { - exts.ensure_total_capacity(OUT_EXTENSIONS_LIST.len() * 2) - .expect("OOM"); - for &ext in OUT_EXTENSIONS_LIST { - exts.put_static_key(ext, b".mjs").expect("OOM"); - } - } else { - exts.ensure_total_capacity(OUT_EXTENSIONS_LIST.len() + 1) - .expect("OOM"); - exts.put_static_key(b".mjs", b".js").expect("OOM"); - } +fn out_extensions(target: Target) -> StringHashMap<&'static [u8]> { + let mut exts = StringHashMap::<&'static [u8]>::default(); + + const OUT_EXTENSIONS_LIST: &[&[u8]] = &[ + b".js", b".cjs", b".mts", b".cts", b".ts", b".tsx", b".jsx", b".json", + ]; + if target == Target::Node { + exts.ensure_total_capacity(OUT_EXTENSIONS_LIST.len() * 2) + .expect("OOM"); for &ext in OUT_EXTENSIONS_LIST { - exts.put_static_key(ext, b".js").expect("OOM"); + exts.put_static_key(ext, b".mjs").expect("OOM"); } + } else { + exts.ensure_total_capacity(OUT_EXTENSIONS_LIST.len() + 1) + .expect("OOM"); + exts.put_static_key(b".mjs", b".js").expect("OOM"); + } - exts + for &ext in OUT_EXTENSIONS_LIST { + exts.put_static_key(ext, b".js").expect("OOM"); } + + exts } pub use bun_options_types::Format; @@ -1301,7 +1338,7 @@ pub struct BundleOptions<'a> { // directly — all access goes through crate::dispatch::DevServerVTable. pub dev_server: *const (), /// Set when Bake is bundling. Affects module resolution. - pub framework: Option<&'a crate::bake_types::Framework>, + pub framework: Option<&'a Framework>, pub serve_plugins: Option]>>, pub bunfig_path: Box<[u8]>, @@ -1667,7 +1704,7 @@ impl<'a> BundleOptions<'a> { production: false, output_format: Format::Esm, tsconfig_override: None, - main_fields: owned_string_list(Target::default_main_fields_map()[Target::Browser]), + main_fields: owned_string_list(default_main_fields_map()[Target::Browser]), allow_unresolved: AllowUnresolved::All, entry_naming: Box::default(), asset_naming: Box::default(), @@ -1767,7 +1804,7 @@ impl<'a> BundleOptions<'a> { if let Some(t) = transform.target { opts.target = ::from_api(Some(t)); - opts.main_fields = owned_string_list(Target::default_main_fields_map()[opts.target]); + opts.main_fields = owned_string_list(default_main_fields_map()[opts.target]); } { @@ -1776,7 +1813,7 @@ impl<'a> BundleOptions<'a> { // 2. node-addons // 3. user conditions opts.conditions = ESMConditions::init( - Target::default_conditions_map()[opts.target], + default_conditions_map()[opts.target], transform.allow_addons.unwrap_or(true), &transform .conditions @@ -1839,7 +1876,7 @@ impl<'a> BundleOptions<'a> { opts.log_mut(), opts.target, ); - opts.out_extensions = opts.target.out_extensions(); + opts.out_extensions = out_extensions(opts.target); opts.source_map = SourceMapOption::from_api(transform.source_map); diff --git a/src/bundler/transpiler.rs b/src/bundler/transpiler.rs index c0b455f7fa54..b7a676af645c 100644 --- a/src/bundler/transpiler.rs +++ b/src/bundler/transpiler.rs @@ -1093,28 +1093,8 @@ fn resolver_bundle_options_subset( }, external: src.external.clone(), extra_cjs_extensions: src.extra_cjs_extensions.clone(), - framework: src.framework.map(|f| { - // Bundler-local `bake_types::BuiltInModule` and - // `bun_options_types::BuiltInModule` are nominally distinct (the - // former predates the TYPE_ONLY move-down); convert variant-wise. - use crate::bake_types::BuiltInModule as B; - use bun_options_types::BuiltInModule as R; - let mut m = bun_collections::StringArrayHashMap::default(); - for (k, v) in f - .built_in_modules - .keys() - .iter() - .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()), - }; - m.put(k, rv).expect("oom"); - } - ropts::Framework { - built_in_modules: m, - } + framework: src.framework.map(|f| ropts::Framework { + built_in_modules: f.built_in_modules.clone().expect("oom"), }), global_cache: src.global_cache, // Both sides store diff --git a/src/options_types/context.rs b/src/options_types/context.rs index 4190731b8ed8..2ef2c92fb37a 100644 --- a/src/options_types/context.rs +++ b/src/options_types/context.rs @@ -210,9 +210,15 @@ pub struct BundlerOptions { pub banner: Box<[u8]>, pub footer: Box<[u8]>, pub css_chunking: bool, - pub bake: bool, - pub bake_debug_dump_server: bool, - pub bake_debug_disable_minify: bool, + /// `bun build --app`: bundle a full-stack application (framework-driven + /// client + server graphs) instead of a plain entry-point build. + pub app: bool, + /// `--debug-dump-server-files` (canary/debug builds only): write the + /// server-side bundle of an `--app` build to disk for inspection. + pub debug_dump_server_files: bool, + /// `--debug-no-minify` (canary/debug builds only): disable minification + /// in an `--app` production build. + pub debug_no_minify: bool, pub production: bool, @@ -264,9 +270,9 @@ impl Default for BundlerOptions { banner: Box::default(), footer: Box::default(), css_chunking: false, - bake: false, - bake_debug_dump_server: false, - bake_debug_disable_minify: false, + app: false, + debug_dump_server_files: false, + debug_no_minify: false, production: false, env_behavior: api::DotEnvBehavior::disable, env_prefix: Box::default(), diff --git a/src/options_types/lib.rs b/src/options_types/lib.rs index 41e3962eae45..524b719d0f93 100644 --- a/src/options_types/lib.rs +++ b/src/options_types/lib.rs @@ -26,6 +26,13 @@ pub use bundle_enums::{ TargetExt, WindowsOptions, }; +/// URL prefix the dev server serves bundled assets under (`"/_bun" ++ "/asset"`). +/// +/// MOVE_DOWN from `bun_runtime::bake::dev_server` so the bundler can emit +/// dev-server asset URLs (`ParseTask`, `bundle_v2`) without referencing bake; +/// `bake::dev_server` re-exports it as `ASSET_PREFIX`. +pub const DEV_SERVER_ASSET_PREFIX: &str = "/_bun/asset"; + /// Compiled-standalone-binary virtual filesystem path prefix + predicate. /// /// MOVE_DOWN from `bun_standalone_graph` (which sits above `bun_resolver` via diff --git a/src/runtime/api/BunObject.rs b/src/runtime/api/BunObject.rs index bc222da43e65..91151b03e87d 100644 --- a/src/runtime/api/BunObject.rs +++ b/src/runtime/api/BunObject.rs @@ -1459,7 +1459,7 @@ fn serve(global_object: &JSGlobalObject, callframe: &CallFrame) -> JsResult { // `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`). +// (see `DevServer::from_server_config`). // The fields `client_graph`, `server_graph`, `directory_watchers`, and `assets` // all use `@fieldParentPointer` to access DevServer's state. This pattern has @@ -473,6 +478,156 @@ impl DeferredPromise { } } +impl DevServer { + /// Build the dev server for a `Bun.serve` instance whose config carried + /// `app` options (`Ok(None)` when it didn't). Consumes the framework and + /// bundler options out of `config.dev_server_options`; the arena that + /// backs `root` stays in the boxed options behind that handle, which must + /// outlive the returned `DevServer` (it lives in the server's config for + /// the server's lifetime). + pub fn from_server_config( + config: &mut crate::server::ServerConfig, + ) -> JsResult>> { + let broadcast_console_log_from_browser_to_server = + config.broadcast_console_log_from_browser_to_server_for_bake; + let Some(handle) = &mut config.dev_server_options else { + return Ok(None); + }; + let bake_options = bake::UserOptions::from_erased_mut(handle); + init(Options { + arena: &bake_options.arena, + root: bake_options.root, + // Per-thread VM singleton; STATIC lifetime. + vm: VirtualMachine::get(), + // LAYERING: `UserOptions` carries the `bake_body` shapes; + // `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(), + broadcast_console_log_from_browser_to_server, + }) + .map(Some) + } +} + +// ─── Host slot-seam bodies ─────────────────────────────────────────────────── +// `NewServer.dev_server` is `Option` — an +// erased `(ptr, vtable)` pair owned by the host. These are the concrete +// halves: the hook below boxes the dev server into the slot, the vtable +// downcasts it for the host's calls, and the `Deref` impls are the typed +// views the per-request paths (`as_deref{,_mut}` callers) consume. + +// Every body downcasts under the same slot contract: per +// `DevServerSlot::from_raw`, `ptr` is the `Box` leaked by +// `__bun_dev_server_from_server_config` below, and the calling slot carries +// the (JS-thread) access claim. +static DEV_SERVER_SLOT_VTABLE: crate::server::DevServerSlotVTable = + crate::server::DevServerSlotVTable { + // SAFETY: slot contract above; the slot's single drop call retakes the + // leaked box. + drop_fn: |ptr| drop(unsafe { Box::from_raw(ptr.cast::().as_ptr()) }), + // SAFETY: slot contract above. + memory_cost: |ptr| unsafe { ptr.cast::().as_ref() }.memory_cost(), + set_inspector_server_id: |ptr, id| { + // SAFETY: slot contract above. + unsafe { ptr.cast::().as_mut() }.inspector_server_id = id; + }, + is_allowed_host: |ptr, req| { + // SAFETY: slot contract above. + unsafe { ptr.cast::().as_ref() }.is_allowed_host(req) + }, + put_html_route: |ptr, path, route| { + // SAFETY: slot contract above. + unsafe { ptr.cast::().as_mut() } + .html_router + .put(path, route) + }, + set_routes: |ptr, server| { + // SAFETY: slot contract above. + let dev = unsafe { ptr.cast::().as_mut() }; + // Un-erase the `(SSL, DEBUG)` monomorphization the host dispatched + // away; `dev` and the server are disjoint heap allocations, so the + // two `&mut`s do not alias. + crate::server::any_server_dispatch_mut!(server, |s| dev.set_routes(s)) + }, + }; + +/// Builds the dev server for a `Bun.serve` instance whose config carried +/// dev-server options, boxed behind the host's erased slot (`Ok(None)` when +/// the config carries none). +#[unsafe(no_mangle)] +fn __bun_dev_server_from_server_config( + config: &mut crate::server::ServerConfig, +) -> JsResult> { + Ok(DevServer::from_server_config(config)?.map(|dev| { + let ptr = ::core::ptr::NonNull::from(Box::leak(dev)).cast::<()>(); + // SAFETY: `ptr` owns the boxed `DevServer` the vtable bodies downcast + // to; the slot's `drop_fn` reboxes exactly that allocation. + unsafe { crate::server::DevServerSlot::from_raw(ptr, &DEV_SERVER_SLOT_VTABLE) } + })) +} + +// The typed views over the host's erased slot: field/method access through +// `Option::as_deref{,_mut}` on `NewServer.dev_server` lands here. This is the +// only downcast of the slot pointer. +impl ::core::ops::Deref for crate::server::DevServerSlot { + type Target = DevServer; + + fn deref(&self) -> &DevServer { + // SAFETY: every slot is constructed by + // `__bun_dev_server_from_server_config` with a leaked `Box`. + unsafe { self.as_ptr().cast::().as_ref() } + } +} + +impl ::core::ops::DerefMut for crate::server::DevServerSlot { + fn deref_mut(&mut self) -> &mut DevServer { + // SAFETY: see `Deref`; `&mut self` carries the slot's exclusive claim. + unsafe { self.as_ptr().cast::().as_mut() } + } +} + +// `AnyServer`'s dev-server accessors are defined here rather than in +// `server/mod.rs` so the server module doesn't name `DevServer`: +// `NewServer.dev_server` is the host's slot, these are the typed views +// over it for the request paths that consult the dev server. +impl AnyServer { + pub fn dev_server(&self) -> Option<&DevServer> { + crate::server::any_server_dispatch!(self, |s| s.dev_server.as_deref()) + } + + /// Mutable handle to the DevServer (when configured). HTMLBundle's request + /// path mutates DevServer state (`respond_for_html_bundle`). + #[allow(clippy::mut_from_ref)] // dispatched through the tagged raw `self.ptr` + pub fn dev_server_mut(&self) -> Option<&mut DevServer> { + crate::server::any_server_dispatch_mut!(self, |s| s.dev_server.as_deref_mut()) + } +} + +// Likewise for `AnyRequestContext`: the typed views over its erased +// `dev_server_ptr()` accessor, for the JS entry points below that recover the +// dev server from a `Request`. +impl crate::server::AnyRequestContext { + pub fn dev_server(self) -> Option<&'static DevServer> { + // SAFETY: every slot pointer is the `Box` leaked by + // `__bun_dev_server_from_server_config`; the server backref outlives + // any `AnyRequestContext` (held only for the duration of a request + // callback), and `self` is a by-value tagged pointer, so there is no + // input lifetime to tie the borrow to. + self.dev_server_ptr() + .map(|ptr| unsafe { ptr.cast::().as_ref() }) + } + + /// Mutable access to the attached DevServer. The accessor above hands out + /// `&` only. The boxed dev server behind the slot has a stable address, so + /// deriving `&mut` from this is sound as long as the caller upholds the + /// usual single-writer rule on the JS thread. + pub fn dev_server_mut(self) -> Option<*mut DevServer> { + self.dev_server_ptr() + .map(|ptr| ptr.cast::().as_ptr()) + } +} + /// DevServer is stored on the heap, storing its allocator. pub(crate) fn init(options: Options) -> JsResult> { // Note: `Features.dev_server +|= 1` (saturating add). AtomicUsize has @@ -1362,16 +1517,18 @@ pub(super) enum DevHandlerId { Request, } -/// DNS-rebinding guard for `/_bun/...` internal routes and the Chrome -/// DevTools `/.well-known/...` route. A rebound origin -/// (`attacker.com` → 127.0.0.1) presents `Host: attacker.com`; rejecting -/// non-loopback / non-IP / non-configured hostnames prevents the attacker's -/// page from reading bundled source via same-origin fetch. -pub(crate) fn is_allowed_dev_host(dev: &DevServer, req: &Request) -> bool { - is_allowed_host_header( - req, - dev.server.as_ref().map(|server| &server.config().address), - ) +impl DevServer { + /// DNS-rebinding guard for `/_bun/...` internal routes and the Chrome + /// DevTools `/.well-known/...` route. A rebound origin + /// (`attacker.com` → 127.0.0.1) presents `Host: attacker.com`; rejecting + /// non-loopback / non-IP / non-configured hostnames prevents the attacker's + /// page from reading bundled source via same-origin fetch. + pub(crate) fn is_allowed_host(&self, req: &Request) -> bool { + is_allowed_host_header( + req, + self.server.as_ref().map(|server| &server.config().address), + ) + } } pub(crate) fn is_allowed_host_header( @@ -1438,7 +1595,7 @@ fn host_without_port(host: &[u8]) -> &[u8] { /// from the same-origin policy, so any page the developer visits could open /// `ws://localhost:/_bun/hmr` and subscribe to hot-update payloads (the /// bundled source) — the browser still sends `Host: localhost`, so -/// `is_allowed_dev_host` alone does not stop it. Browsers always include an +/// `is_allowed_host` alone does not stop it. Browsers always include an /// `Origin` header on WebSocket handshakes; require its host to be the /// request's own host or a localhost name. Requests without an `Origin` /// header (non-browser clients) are allowed. @@ -1513,7 +1670,7 @@ extern "C" fn dev_route_tramp( }; // SAFETY: uWS passes a non-null `Request*` valid for the callback; shared, // call-scoped reborrow. - if !is_allowed_dev_host(unsafe { &*dev }, unsafe { &*req }) { + if !unsafe { &*dev }.is_allowed_host(unsafe { &*req }) { return host_forbidden(resp); } // SAFETY: as above. @@ -1638,7 +1795,7 @@ impl bun_uws_sys::web_socket::WebSocketUpgradeServer for D // likewise statement-scoped. // // SAFETY: `this` is the live DevServer registered for the upgrade callback. - if !is_allowed_dev_host(unsafe { &*this }, req) { + if !unsafe { &*this }.is_allowed_host(req) { // SAFETY: `res` is live for this callback (see Note above). host_forbidden(unsafe { &mut *res }.as_any_response()); return; @@ -2000,7 +2157,7 @@ fn ensure_route_is_bundled( .as_ref() .expect("infallible: server bound") .get_or_load_plugins( - crate::server::ServePluginsCallback::DevServer(dev), + crate::server::ServePluginsCallback::Consumer(dev), ); match load_result { crate::server::GetOrStartLoadResult::Pending => { @@ -2188,6 +2345,10 @@ impl DevServer { unsafe { &mut *r }, resp, global, + // Materialize the JS request through bake's + // `JSBunRequest` wrapper (carries the route + // params object). + CreateJsRequest::Custom(WebRequest::to_js_for_bake), Some(method), )? { Some(saved) => saved, @@ -2676,6 +2837,9 @@ impl DevServer { args.bundle_new_route, args.new_route_params, ], + // Materialize the JS request through bake's `JSBunRequest` + // wrapper (carries the route params object). + CreateJsRequest::Custom(WebRequest::to_js_for_bake), ); Ok(()) } @@ -3210,8 +3374,9 @@ impl DevServer { let mut bv2: Box> = BundleV2::init( // SAFETY: `server_transpiler` outlives `bv2` (held by `self`). unsafe { (*self_ptr).server_transpiler.assume_init_mut() }, - Some(bundler::bundle_v2::BakeOptions { + Some(bundler::bundle_v2::FrameworkBundleOptions { framework: self.framework.as_bundler_view(), + server_component_manifests: super::SERVER_COMPONENTS_MANIFESTS, // SAFETY: sibling fields of `*self`; `BundleV2` stores them as // raw pointers and never moves them. client_transpiler: unsafe { @@ -3238,7 +3403,7 @@ impl DevServer { bv2.asynchronous = true; let dev_handle = self.bundler_handle(); bv2.dev_server = Some(dev_handle); - bv2.linker.dev_server = Some(dev_handle); + bv2.linker.has_dev_server = true; { self.graph_safety_lock.lock(); @@ -3247,17 +3412,14 @@ impl DevServer { self.graph_safety_lock.unlock(); } - // LAYERING: `bun_bundler::bake_types::EntryPointList` is the TYPE_ONLY + // LAYERING: `bun_bundler::bundle_v2::EntryPointList` is the TYPE_ONLY // mirror of this file's `EntryPointList` (moved down so `bun_bundler` // can name it without depending on `bun_runtime`). Convert by value — // both `Flags` are `#[repr(transparent)] u8` with identical bit layout. - let start_data = bv2.start_from_bake_dev_server(&{ - let mut bt = bundler::bake_types::EntryPointList::empty(); + let start_data = bv2.start_from_dev_server(&{ + let mut bt = bundler::bundle_v2::EntryPointList::empty(); for (k, v) in entry_points.set.iter() { - bun_core::handle_oom( - bt.set - .put(k, bundler::bake_types::EntryPointFlags(v.bits())), - ); + bun_core::handle_oom(bt.set.put(k, bundler::bundle_v2::EntryPointFlags(v.bits()))); } bt })?; @@ -5155,7 +5317,7 @@ impl DevServer { req: &mut Request, resp: AnyResponse, ) -> Result<(), AllocError> { - if !is_allowed_dev_host(self, req) { + if !self.is_allowed_host(req) { host_forbidden(resp); return Ok(()); } @@ -6126,8 +6288,10 @@ impl DevServer { )?; Ok(()) } +} - pub(crate) fn on_plugins_resolved( +impl crate::server::ServePluginsConsumer for DevServer { + fn on_plugins_resolved( &mut self, plugins: Option<*mut crate::api::js_bundler::Plugin>, ) -> crate::Result<()> { @@ -6137,7 +6301,7 @@ impl DevServer { Ok(()) } - pub(crate) fn on_plugins_rejected(&mut self) -> crate::Result<()> { + fn on_plugins_rejected(&mut self) -> crate::Result<()> { self.plugin_state = PluginState::Err; while let Some(item) = self.next_bundle.requests.pop_first() { // SAFETY: `pop_first` returns a valid `*mut Node`; diff --git a/src/runtime/bake/FrameworkRouter.classes.ts b/src/runtime/bake/FrameworkRouter.classes.ts new file mode 100644 index 000000000000..4aa33b1be7c5 --- /dev/null +++ b/src/runtime/bake/FrameworkRouter.classes.ts @@ -0,0 +1,25 @@ +import { define } from "../../codegen/class-definitions"; + +export default [ + define({ + name: "FrameworkFileSystemRouter", + // JS name and Rust type name differ, so the name-based resolver can't + // find the backing struct on its own. + rustPath: "crate::bake::framework_router::JSFrameworkRouter", + construct: true, + finalize: true, + JSType: "0b11101110", + configurable: false, + proto: { + toJSON: { + fn: "toJSON", + length: 0, + }, + match: { + fn: "match", + length: 1, + }, + }, + klass: {}, + }), +]; diff --git a/src/runtime/bake/FrameworkRouter.rs b/src/runtime/bake/FrameworkRouter.rs index ba6c27270bfa..d29b3b8ac03e 100644 --- a/src/runtime/bake/FrameworkRouter.rs +++ b/src/runtime/bake/FrameworkRouter.rs @@ -652,6 +652,82 @@ impl Style { } } +// The server's route-parsing context stores the framework-router collection +// state behind the `FrameworkRouterTypes` projection so `server_body.rs` +// never names this module's types; this impl supplies the concrete types. +impl crate::server::FrameworkRouterTypes for crate::server::FrameworkRouterSeam { + type Mount = crate::bake::FileSystemRouterType; + type StringAllocations = crate::bake::StringRefList; +} + +// Sibling projection consumed by `AnyRoute::FrameworkRouter`'s payload +// (`server/mod.rs`); supplied here for the same reason as above. +impl crate::server::FrameworkRouterRouteTypes for crate::server::FrameworkRouterSeam { + type TypeIndex = TypeIndex; +} + +// Implemented here rather than in `server_body.rs` so the server's route +// parser stays agnostic of framework-router semantics (style parsing, the +// bun-framework-react defaults, and the router-count limit). +impl crate::server::ServerInitContext<'_> { + /// Register a `{ dir, style }` route as a framework-router mount. `path` + /// has already been validated to end in `/*`; a style-less `{ dir }` is a + /// `DirectoryRoute` and never reaches this. + pub(crate) fn framework_router_from_js( + &mut self, + global: &JSGlobalObject, + path: &[u8], + relative_root: &[u8], + style_js: JSValue, + ) -> JsResult { + let style: Style = Style::from_js(style_js, global)?; + // 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"/") + } else { + Cow::Owned(path[..path.len() - 2].to_vec()) + }; + self.framework_router_list + .push(crate::bake::FileSystemRouterType { + root: Cow::Owned(relative_root.to_vec()), + 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"), + ignore_underscores: true, + ignore_dirs: vec![ + Cow::Borrowed(b"node_modules".as_slice()), + Cow::Borrowed(b".git".as_slice()), + ], + extensions: vec![ + Cow::Borrowed(b".tsx".as_slice()), + Cow::Borrowed(b".jsx".as_slice()), + ], + allow_layouts: true, + }); + + // `@typeInfo(FrameworkRouter.Type.Index).@"enum".tag_type` → the index newtype's backing-int MAX. + let limit = u8::MAX as usize; + if self.framework_router_list.len() > limit { + return Err(global.throw_invalid_arguments(format_args!( + "Too many framework routers. Maximum is {}.", + limit + ))); + } + Ok(TypeIndex::init( + u8::try_from(self.framework_router_list.len() - 1).expect("int cast"), + )) + } +} + #[derive(Copy, Clone, Eq, PartialEq, core::marker::ConstParamTy)] pub(crate) enum UiOrRoutes { Ui, diff --git a/src/runtime/bake/bake_body.rs b/src/runtime/bake/bake_body.rs index d51f80223766..4ea08397ea3a 100644 --- a/src/runtime/bake/bake_body.rs +++ b/src/runtime/bake/bake_body.rs @@ -27,6 +27,9 @@ use crate::api::js_bundler::js_bundler::PluginJscExt as _; // matching filename). use super::{dev_server, framework_router}; +use crate::server::server_config::DevelopmentOption; +use crate::server::{DevServerOptions, ServerInitContext}; + // Note: `pub use dev_server as DevServer` / `framework_router as // FrameworkRouter` are already provided by the parent `mod.rs` (lines 349/369); // re-exporting here triggers E0365 because `bake_body` is a private module. @@ -258,6 +261,207 @@ impl UserOptions { arena, }) } + + /// Build the dev-server options for `Bun.serve` when HTML imports or + /// framework routers appear in the `routes` object. `router_types` and + /// `allocations` are collected by the server's route parsing; the + /// framework (via [`Framework::auto`]) and the per-graph env/define + /// bundler options are derived here from the VM's transpiler options. + pub fn from_serve_routes( + global: &JSGlobalObject, + router_types: Vec, + allocations: StringRefList, + ) -> JsResult { + // NOTE: the arena is created here and moved into `UserOptions` + // (lives until the options are dropped). + let arena = Arena::new(); + + let root = arena_dupe_z(&arena, paths::fs::FileSystem::instance().top_level_dir()); + + // Convert the keystone `bake::FileSystemRouterType` (Cow-backed) into + // the body shape (`&'static` slices) by duping every string into the + // arena. Type duplication; remove once the two structs unify. + let router_types: Vec = router_types + .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 = Framework::auto(&arena, resolver, router_types) + .map_err(|e| throw_core_error(global, e, "Framework::auto"))?; + + let mut user_options = UserOptions { + arena, + allocations, + root, + framework, + bundler_options: SplitBundlerOptions::default(), + }; + + use bun_schema::api::DotEnvBehavior; + let o = &global.bun_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| arena_dupe_z(&user_options.arena, p).as_bytes()); + user_options.bundler_options.client.env = DotEnvBehavior::prefix; + } + DotEnvBehavior::load_all => { + user_options.bundler_options.client.env = DotEnvBehavior::load_all; + } + DotEnvBehavior::disable => { + user_options.bundler_options.client.env = DotEnvBehavior::disable; + } + _ => {} + } + + if let Some(define) = &o.serve_define { + user_options.bundler_options.client.define = define.clone(); + user_options.bundler_options.server.define = define.clone(); + user_options.bundler_options.ssr.define = define.clone(); + } + + Ok(user_options) + } + + /// Box and erase into the server's opaque options slot + /// (`ServerConfig::dev_server_options`). Paired with + /// [`UserOptions::from_erased_mut`]. + fn erase(self) -> DevServerOptions { + unsafe fn drop_erased(ptr: NonNull<()>) { + // SAFETY: `ptr` came from `Box::into_raw` in `erase`, which + // transferred ownership to the handle calling us. + drop(unsafe { Box::from_raw(ptr.cast::().as_ptr()) }); + } + // SAFETY: `Box::into_raw` never returns null. + let ptr = unsafe { NonNull::new_unchecked(Box::into_raw(Box::new(self))) }; + // SAFETY: ownership of the freshly boxed `UserOptions` transfers to + // the handle; `drop_erased` frees exactly that value once. + unsafe { DevServerOptions::from_raw(ptr.cast(), drop_erased) } + } + + /// Downcast the server's opaque options slot back to the concrete type. + pub(crate) fn from_erased_mut(handle: &mut DevServerOptions) -> &mut UserOptions { + // SAFETY: per `DevServerOptions::from_raw`'s contract the only + // constructors are the `__bun_bake_dev_server_options_*` hooks below, + // which always erase a boxed `UserOptions` via `erase`; `&mut handle` + // uniquely borrows the box. + unsafe { &mut *handle.as_ptr().cast::().as_ptr() } + } +} + +// ─── `Bun.serve` options seam ──────────────────────────────────────────────── +// CYCLEBREAK extern hooks: `ServerConfig::from_js` reaches the two +// `UserOptions` constructors through these link-time hooks (declared in the +// dev-server seam section of `server/mod.rs`, same pattern as +// `__bun_bake_convert_stmts_for_chunk_hmr` in `hmr_module_format.rs`) so the +// server never names the concrete options type. + +/// Derive dev-server options from the HTML bundles and framework routers +/// collected while parsing the `routes` object. `Ok(None)` when the routes +/// need no dev server: nothing was collected, or HMR is disabled and no +/// framework routers are present. +#[unsafe(no_mangle)] +fn __bun_bake_dev_server_options_from_serve_routes( + init_ctx: &mut ServerInitContext<'_>, + development: DevelopmentOption, +) -> JsResult> { + if init_ctx.dedupe_html_bundle_map.is_empty() && init_ctx.framework_router_list.is_empty() { + return Ok(None); + } + if development.is_hmr_enabled() { + let options = UserOptions::from_serve_routes( + init_ctx.global, + core::mem::take(&mut init_ctx.framework_router_list), + core::mem::take(&mut init_ctx.js_string_allocations), + )?; + Ok(Some(options.erase())) + } else if !init_ctx.framework_router_list.is_empty() { + Err(init_ctx.global.throw_invalid_arguments(format_args!( + "FrameworkRouter is currently only supported when `development: true`", + ))) + } else { + Ok(None) + } +} + +/// Read and parse the `app` option from the full `Bun.serve` options object. +/// `Ok(None)` when the bake feature flag is disabled (the options object is +/// not touched, so no user getter runs) or `app` is absent/falsy; errors when +/// dev-server options were already derived from `routes` or `development` is +/// `Production`. +#[unsafe(no_mangle)] +fn __bun_bake_dev_server_options_from_app( + serve_options: JSValue, + global: &JSGlobalObject, + has_existing_options: bool, + development: DevelopmentOption, +) -> JsResult> { + if !super::is_enabled() { + return Ok(None); + } + let Some(app) = serve_options.get_truthy(global, "app")? else { + return Ok(None); + }; + if has_existing_options { + // "app" is likely to be removed in favor of the HTML loader. + return Err( + global.throw_invalid_arguments(format_args!("'app' + HTML loader not supported.",)) + ); + } + if development == DevelopmentOption::Production { + return Err(global.throw_invalid_arguments(format_args!( + "TODO: 'development: false' in serve options with 'app'. For now, use `bun build --app` or set 'development: true'", + ))); + } + Ok(Some(UserOptions::from_js(app, global)?.erase())) +} + +/// Bridge the keystone `bake::FileSystemRouterType` (Cow-backed, populated by +/// `server_body::AnyRoute::from_js`) into the body `FileSystemRouterType` +/// (`&'static [u8]`-backed, consumed by `Framework::auto`). The duplication is +/// a layering wart and this conversion stands in for an arena-dupe until the +/// two structs unify. All bytes are duped into `arena` so the resulting +/// `&'static` slices live as long as `UserOptions.arena`. +fn convert_file_system_router_type( + arena: &Arena, + src: super::FileSystemRouterType, +) -> FileSystemRouterType { + // NOTE: `arena_erase` is the single sanctioned `'bump → 'static` erasure + // for the `UserOptions.arena` self-referential pattern; `Framework::from_js` + // / `resolve` use it identically. + // TODO(refactor): thread a real `'bump` through `Framework`/ + // `FileSystemRouterType` and remove this together with `arena_erase`. + fn dupe(arena: &Arena, bytes: &[u8]) -> &'static [u8] { + arena_erase(arena.alloc_slice_copy(bytes)) + } + fn dupe_slice_of( + arena: &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(); + arena_erase(arena.alloc_slice_copy(&inner)) + } + + 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, + } } /// Each string stores its allocator since some may hold reference counts to JSC @@ -1436,10 +1640,25 @@ pub(crate) fn get_hmr_runtime(side: Side) -> HmrRuntime { }) } +/// CYCLEBREAK extern hook: the bundler's chunk codegen +/// (`postProcessJSChunk`) splices the HMR runtime preamble for +/// `Format::InternalBakeDev` output but cannot depend on this crate; it +/// reaches the embedded bytes through this link-time hook (declared in +/// `bun_bundler::bake_types`, next to the `DevServerHandle` seam). The +/// NUL-terminated `&ZStr` flavour stays private to bake for JSC handoff; the +/// bundler view is the plain byte slice (NUL excluded). +#[unsafe(no_mangle)] +fn __bun_bake_get_hmr_runtime(side: Side) -> bun_bundler::bake_types::HmrRuntime { + let rt = get_hmr_runtime(side); + bun_bundler::bake_types::HmrRuntime { + code: rt.code.as_bytes(), + } +} + // Note: `Mode`/`Side`/`Graph` are defined canonically in the parent // `bake/mod.rs` (which itself re-exports `Side`/`Graph` from // `bun_bundler::bake_types`). Re-export here so `bake_body::Mode` ≡ -// `crate::bake::Mode` and downstream callers (production.rs, build_command.rs, +// `crate::bake::Mode` and downstream callers (production.rs, // IncrementalGraph.rs) see one nominal type. pub(crate) use super::Mode; pub(crate) use bun_bundler::bake_types::{Graph, Side}; diff --git a/src/runtime/bake/dev_server/hmr_socket.rs b/src/runtime/bake/dev_server/hmr_socket.rs index 38ef17a2be0a..4d0ee928d0c0 100644 --- a/src/runtime/bake/dev_server/hmr_socket.rs +++ b/src/runtime/bake/dev_server/hmr_socket.rs @@ -1,6 +1,6 @@ use bun_collections::HashMap; +use bun_core::Output; use bun_core::strings; -use bun_core::{Output, feature_flags}; use bun_uws::AnyWebSocket; use bun_uws_sys::{Opcode, SendStatus}; @@ -108,7 +108,7 @@ impl HmrSocket { let _ = ws.subscribe(&field.uws_topic()); // on-subscribe hooks - if feature_flags::BAKE_DEBUGGING_FEATURES { + if crate::bake::DEBUGGING_FEATURES { // SAFETY: JS-thread only; sole `&mut DevServer` for this scope. let dev = unsafe { self.dev() }; match field { @@ -301,7 +301,7 @@ impl HmrSocket { } fn on_unsubscribe(&mut self, field: HmrTopicBits) { - if feature_flags::BAKE_DEBUGGING_FEATURES { + if crate::bake::DEBUGGING_FEATURES { // SAFETY: JS-thread only; sole `&mut DevServer` for this scope. let dev = unsafe { self.dev() }; if field.contains(HmrTopic::IncrementalVisualizer.as_bit()) { diff --git a/src/runtime/bake/dev_server/mod.rs b/src/runtime/bake/dev_server/mod.rs index 71a4951daf9b..652f57800d63 100644 --- a/src/runtime/bake/dev_server/mod.rs +++ b/src/runtime/bake/dev_server/mod.rs @@ -30,7 +30,9 @@ pub(crate) mod memory_cost; // re-exported via the `pub use` block below alongside the `struct DevServer` // type. Declaring it again here would collide in the value namespace. -pub(crate) const ASSET_PREFIX: &str = "/_bun/asset"; +/// Canonical definition lives in `bun_options_types` (T3) so the bundler can +/// emit dev-server asset URLs without referencing bake. +pub(crate) use bun_options_types::DEV_SERVER_ASSET_PREFIX as ASSET_PREFIX; pub(crate) const CLIENT_PREFIX: &str = "/_bun/client"; // LAYERING: the 4.8 kL of method bodies live in `../DevServer.rs` (mounted as @@ -44,7 +46,7 @@ pub use super::dev_server_body::{ TestingBatchEvents, deferred_request, entry_point_list, }; -/// `DevServer.FileKind` — kept in lockstep with `bun_bundler::bake_types::CacheKind` +/// `DevServer.FileKind` — kept in lockstep with `bun_bundler::CacheKind` /// (the vtable boundary maps between them via an exhaustive match). #[repr(u8)] #[derive(Copy, Clone, Eq, PartialEq, Debug)] @@ -267,8 +269,6 @@ impl GraphTraceState { } } -pub(crate) use super::dev_server_body::init; - pub mod assets; pub mod incremental_graph; pub mod inspector_agent; @@ -1144,8 +1144,11 @@ pub mod directory_watch_store { bun_bundler::link_impl_DevServerHandle! { Bake for DevServer => |this| { barrel_needed_exports() => &raw mut (*this).barrel_needed_exports, - log_for_resolution_failures(abs_path, graph) => { - match (*this).get_log_for_resolution_failures(abs_path, graph) { + log_for_resolution_failures(abs_path, target) => { + match (*this).get_log_for_resolution_failures( + abs_path, + bun_bundler::bake_types::TargetExt::bake_graph(target), + ) { Ok(log) => log, Err(_) => bun_alloc::out_of_memory(), } @@ -1157,9 +1160,15 @@ bun_bundler::link_impl_DevServerHandle! { super::dev_server_body::finalize_bundle(&mut *this, &mut *bv2.cast(), &mut *result) .map_err(|e| bun_bundler::Error::from(crate::Error::from(e))) }, - handle_parse_task_failure(err, graph, abs_path, log, bv2) => { + handle_parse_task_failure(err, target, abs_path, log, bv2) => { (*this) - .handle_parse_task_failure(&err.into(), graph, abs_path, &*log, &mut *bv2) + .handle_parse_task_failure( + &err.into(), + bun_bundler::bake_types::TargetExt::bake_graph(target), + abs_path, + &*log, + &mut *bv2, + ) .map_err(Into::into) }, put_or_overwrite_asset(path, contents, content_hash) => { @@ -1170,24 +1179,31 @@ bun_bundler::link_impl_DevServerHandle! { let blob = crate::webcore::blob::Any::from_owned_slice(contents.to_vec()); (*this).put_or_overwrite_asset(path, blob, content_hash).map_err(Into::into) }, - track_resolution_failure(import_source, specifier, renderer, loader) => { + track_resolution_failure(import_source, specifier, target, loader) => { (*this) .directory_watchers - .track_resolution_failure(import_source, specifier, renderer, loader) + .track_resolution_failure( + import_source, + specifier, + bun_bundler::bake_types::TargetExt::bake_graph(target), + loader, + ) .map_err(Into::into) }, - is_file_cached(abs_path, side) => { - (*this).is_file_cached(abs_path, side).map(|e| { - use bun_bundler::bake_types::CacheKind; - bun_bundler::bake_types::CacheEntry { - kind: match e.kind { - FileKind::Unknown => CacheKind::Unknown, - FileKind::Js => CacheKind::Js, - FileKind::Asset => CacheKind::Asset, - FileKind::Css => CacheKind::Css, - }, - } - }) + is_file_cached(abs_path, target) => { + (*this) + .is_file_cached(abs_path, bun_bundler::bake_types::TargetExt::bake_graph(target)) + .map(|e| { + use bun_bundler::CacheKind; + bun_bundler::CacheEntry { + kind: match e.kind { + FileKind::Unknown => CacheKind::Unknown, + FileKind::Js => CacheKind::Js, + FileKind::Asset => CacheKind::Asset, + FileKind::Css => CacheKind::Css, + }, + } + }) }, asset_hash(abs_path) => (*this).assets.get_hash(abs_path), current_bundle_start_data() => { diff --git a/src/runtime/bake/dev_server/route_bundle.rs b/src/runtime/bake/dev_server/route_bundle.rs index 8e702edc2670..f12597585401 100644 --- a/src/runtime/bake/dev_server/route_bundle.rs +++ b/src/runtime/bake/dev_server/route_bundle.rs @@ -4,13 +4,15 @@ use super::incremental_graph; use super::jsc; use super::source_map_store; use crate::bake::framework_router; +use crate::server::html_bundle::DevServerRouteId; use crate::server::static_route::InitFromBytesOptions; use crate::server::{StaticRoute, html_bundle::HTMLBundleRoute}; use crate::webcore::AnyBlob; -/// `bun.GenericIndex(u30, RouteBundle)`. -pub enum RouteBundleMarker {} -pub(crate) type Index = bun_core::GenericIndex; +/// `bun.GenericIndex(u30, RouteBundle)`. Nominally the host's opaque +/// [`DevServerRouteId`] token: `getOrPutRouteBundle` stores the index of a +/// route's `RouteBundle` directly in the host's `dev_server_id` slot. +pub(crate) type Index = DevServerRouteId; pub(crate) type IndexOptional = Option; /// `bun.GenericIndex(u32, u8)` — byte offset into `bundled_html_text`. diff --git a/src/bundler/linker_context/convertStmtsForChunkForDevServer.rs b/src/runtime/bake/hmr_module_format.rs similarity index 91% rename from src/bundler/linker_context/convertStmtsForChunkForDevServer.rs rename to src/runtime/bake/hmr_module_format.rs index 17dfac19f76a..fe4967e233a6 100644 --- a/src/bundler/linker_context/convertStmtsForChunkForDevServer.rs +++ b/src/runtime/bake/hmr_module_format.rs @@ -1,5 +1,10 @@ -use crate::BundledAst as JSAst; -use crate::mal_prelude::*; +//! Statement conversion for `Format::InternalBakeDev` output — the packed +//! HMR-module shape decoded by `hmr-module.ts` / `hmr-runtime-client.ts`. +//! Encoder and decoder live together here in bake; the bundler's chunk +//! codegen reaches the encoder through the +//! `__bun_bake_convert_stmts_for_chunk_hmr` link-time hook declared in +//! `bun_bundler` (lib.rs, next to the `DevServerHandle` seam). + use bun_alloc::ArenaVecExt as _; use bun_alloc::{AllocError, Arena as Bump}; use bun_ast as js_ast; @@ -8,9 +13,26 @@ use bun_ast::ImportRecordFlags; use bun_ast::Loc; use bun_ast::{Binding, E, Expr, ExprNodeList, G, S, Stmt, StmtData, b}; use bun_ast::{ImportRecordTag, Loader}; +use bun_bundler::BundledAst as JSAst; +use bun_bundler::linker_context_mod::{StmtList, StmtListWhich}; use bun_collections::VecExt; -use crate::linker_context_mod::{LinkerContext, StmtList, StmtListWhich}; +/// CYCLEBREAK extern hook: called from the bundler's +/// `generate_code_for_file_in_chunk_js` when the output format is +/// `InternalBakeDev`. Defined here (not in `bun_bundler`) so the HMR module +/// encoding lives beside the runtime that decodes it. `loaders`/`sources` are +/// the parse graph's input-file columns, computed once by the caller. +#[unsafe(no_mangle)] +fn __bun_bake_convert_stmts_for_chunk_hmr( + stmts: &mut StmtList, + part_stmts: &[Stmt], + bump: &Bump, + ast: &mut JSAst<'_>, + loaders: &[Loader], + sources: &[bun_ast::Source], +) -> Result<(), AllocError> { + convert_stmts_for_chunk_for_dev_server(stmts, part_stmts, bump, ast, loaders, sources) +} /// For CommonJS, all statements are copied `inside_wrapper_suffix` and this returns. /// The conversion logic is completely different for format .internal_bake_dev @@ -42,21 +64,19 @@ use crate::linker_context_mod::{LinkerContext, StmtList, StmtListWhich}; /// ┃ }; /// }, false ], /// ----- "is the module async?" -pub(crate) fn convert_stmts_for_chunk_for_dev_server<'bump>( - c: &mut LinkerContext, +fn convert_stmts_for_chunk_for_dev_server<'bump>( stmts: &mut StmtList, - part_stmts: &[bun_ast::Stmt], + part_stmts: &[Stmt], bump: &'bump Bump, ast: &mut JSAst<'_>, + loaders: &[Loader], + sources: &[bun_ast::Source], ) -> Result<(), AllocError> { let hmr_api_ref = ast.wrapper_ref; let hmr_api_id = Expr::init_identifier(hmr_api_ref, Loc::EMPTY); let mut esm_decls: bun_alloc::ArenaVec<'bump, ArrayBinding> = bun_alloc::ArenaVec::new_in(bump); let mut esm_callbacks: Vec = Vec::new(); - let input_files = &c.parse_graph().input_files; - let loaders = input_files.items_loader(); - let sources = input_files.items_source(); for record in ast.import_records.as_mut_slice() { if record.path.is_disabled { continue; diff --git a/src/runtime/bake/mod.rs b/src/runtime/bake/mod.rs index 12136842c27d..94a7e1ce7ad0 100644 --- a/src/runtime/bake/mod.rs +++ b/src/runtime/bake/mod.rs @@ -20,7 +20,6 @@ pub(crate) mod bake_body; #[path = "DevServer.rs"] mod dev_server_body; pub(crate) use dev_server_body::get_deinit_count_for_testing; -pub(crate) use dev_server_body::is_allowed_dev_host; pub(crate) use dev_server_body::is_allowed_host_header; #[path = "FrameworkRouter.rs"] @@ -29,6 +28,11 @@ pub(crate) mod framework_router_body; #[path = "production.rs"] mod production_body; +// `Format::InternalBakeDev` statement conversion (the packed HMR-module +// shape). Reached from the bundler via the +// `__bun_bake_convert_stmts_for_chunk_hmr` link-time hook it defines. +mod hmr_module_format; + // `Bun__add{Bake,DevServer}SourceProvider*` host exports — the Rust side of // `BakeSourceProvider.h` / `DevServerSourceProvider.h`. Reached only via the // codegen-emitted `extern "C"` thunks in `generated_host_exports.rs`. @@ -49,6 +53,19 @@ pub mod jsc { pub(crate) use crate::jsc::*; } +/// Enable the "app" option in Bun.serve. This option will likely be removed +/// in favor of HTML loaders and configuring framework options in bunfig.toml +pub fn is_enabled() -> bool { + // In canary or if an environment variable is specified. + bun_core::env::IS_CANARY + || bun_core::env::IS_DEBUG + || bun_core::feature_flag::BUN_FEATURE_FLAG_EXPERIMENTAL_BAKE.get() +} + +/// Additional debugging features for bake.DevServer, such as the incremental visualizer. +/// To use them, extra flags are passed in addition to this one. +pub const DEBUGGING_FEATURES: bool = bun_core::env::IS_CANARY || bun_core::env::IS_DEBUG; + // ══════════════════════════════════════════════════════════════════════════ // Top-level types // ══════════════════════════════════════════════════════════════════════════ @@ -69,6 +86,19 @@ pub enum Mode { ProductionStatic, } +/// Adds the Vite-style `import.meta.env.*` defines for a development +/// server-components build: server-side values into `server_define` and +/// client-side values into `client_define`. Single entry point for +/// `bun build --server-components`, which always builds in development +/// mode, so the CLI doesn't need to name `Mode`/`Side`. +pub(crate) fn add_dev_server_components_defines( + server_define: &mut bun_bundler::options::Define, + client_define: &mut bun_bundler::options::Define, +) -> crate::Result<()> { + bake_body::add_import_meta_defines(server_define, Mode::Development, Side::Server)?; + bake_body::add_import_meta_defines(client_define, Mode::Development, Side::Client) +} + /// `bake.Framework.ServerComponents`. /// /// String fields are arena-backed at runtime but default to static literals. @@ -141,6 +171,27 @@ impl Default for Framework { } } +/// Bake's names for the two server-components manifest virtual modules, +/// passed to the bundler through +/// `FrameworkBundleOptions.server_component_manifests` (the bundler +/// synthesizes the modules but hardcodes no specifier strings). +/// The `specifier`s are the contract with framework JS +/// (`import ... from "bun:bake/server"`); the `path`s are the stable internal +/// names used for chunk naming and sourcemaps. +pub(crate) const SERVER_COMPONENTS_MANIFESTS: bun_bundler::bundle_v2::ServerComponentsManifests = + bun_bundler::bundle_v2::ServerComponentsManifests { + server: bun_bundler::bundle_v2::VirtualModule { + specifier: b"bun:bake/server", + path: b"_bun/bake/server", + namespace: b"bun", + }, + client: bun_bundler::bundle_v2::VirtualModule { + specifier: b"bun:bake/client", + path: b"_bun/bake/client", + namespace: b"bun", + }, + }; + 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 @@ -479,8 +530,9 @@ pub struct SplitBundlerOptions { // 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]` → +// note in bake_body.rs), bridge by-value here so `DevServer::from_server_config` +// (DevServer.rs) can hand the `UserOptions` behind `config.dev_server_options` +// 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 { @@ -599,12 +651,13 @@ pub(crate) struct HmrRuntime { pub(crate) line_count: u32, } pub(crate) use bake_body::get_hmr_runtime; -// (Former `__bun_bake_get_hmr_runtime` link-time bridge deleted — -// `bun_bundler::bake_types::get_hmr_runtime` now loads the codegen bytes -// itself via `bun_core::runtime_embed_file!`, so the storage moved DOWN and -// the cross-crate hook is gone. This crate's `HmrRuntime` keeps the -// NUL-terminated `&ZStr` form for JSC handoff; the bundler-side one is plain -// `&[u8]`.) +// The codegen'd `bake.client.js` / `bake.server.js` bytes are loaded only +// here (via `bun_core::runtime_embed_file!` in `bake_body::get_hmr_runtime`); +// the bundler's chunk codegen reaches them through the +// `__bun_bake_get_hmr_runtime` link-time hook defined in `bake_body.rs`. +// This crate's `HmrRuntime` keeps the NUL-terminated `&ZStr` form for JSC +// handoff; the bundler-side view (`bun_bundler::bake_types::HmrRuntime`) is +// plain `&[u8]`. pub use bake_body::StringRefList; diff --git a/src/runtime/bake/production.rs b/src/runtime/bake/production.rs index 81c32d956928..e3eceb7d74b2 100644 --- a/src/runtime/bake/production.rs +++ b/src/runtime/bake/production.rs @@ -16,6 +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::bundle_v2; use bun_bundler::options::{self as bundler_options, OutputFile, SourceMapOption}; use bun_bundler::output_file::Index as OutputFileIndex; @@ -473,7 +474,7 @@ fn build_with_vm(ctx: Context, cwd: &[u8], pt: &mut PerThread) -> crate::Result< // inside `if separate_ssr_graph` blocks below — Rust forbids forming // `&mut T` to uninitialized memory regardless of later use. - if ctx.bundler_options.bake_debug_disable_minify { + if ctx.bundler_options.debug_no_minify { let mut targets: Vec<&mut Transpiler> = vec![&mut *client_transpiler, &mut *server_transpiler]; if separate_ssr_graph { @@ -578,8 +579,8 @@ fn build_with_vm(ctx: Context, cwd: &[u8], pt: &mut PerThread) -> crate::Result< // `clone()`). style: fsr.style.clone(), allow_layouts: fsr.allow_layouts, - server_file: OpaqueFileId::init(server_file.get()), - client_file: client_file.map(|f| OpaqueFileId::init(f.get())), + server_file, + client_file, server_file_string: bun_jsc::StrongOptional::empty(), }); } @@ -622,12 +623,13 @@ fn build_with_vm(ctx: Context, cwd: &[u8], pt: &mut PerThread) -> crate::Result< // catch-and-exit here: the bake path expects this call to succeed for // valid inputs, and any `BuildFailed` indicates a bug upstream // (in the bundler), not a user-facing diagnostic to swallow. - BundleV2::generate_from_bake_production_cli( + generate_production_bundle( &entry_points, // SAFETY: see `server_ptr` comment above. unsafe { &mut *server_ptr }, - bun_bundler::bundle_v2::BakeOptions { + bun_bundler::bundle_v2::FrameworkBundleOptions { framework: bundler_framework, + server_component_manifests: super::SERVER_COMPONENTS_MANIFESTS, client_transpiler: NonNull::new(client_ptr).expect("stack-owned transpiler"), ssr_transpiler: NonNull::new(ssr_ptr).expect("stack-owned transpiler"), plugins: options.bundler_options.plugin, @@ -713,7 +715,7 @@ fn build_with_vm(ctx: Context, cwd: &[u8], pt: &mut PerThread) -> crate::Result< } } bun_bundler::options::Side::Server => { - if ctx.bundler_options.bake_debug_dump_server { + if ctx.bundler_options.debug_dump_server_files { if let Err(err) = file.write_to_disk(root_dir.fd(), b".") { bun_core::handle_error_return_trace(err); Output::err( @@ -1378,16 +1380,182 @@ extern "C" fn BakeProdResolve( )) } +/// Drive the bundler pipeline for a Bake production build: enqueue each route +/// entry point on the graph its side selects, parse, link, and generate +/// chunks. Mirrors `BundleV2::generate_from_cli` minus watch mode and the +/// metafile. +fn generate_production_bundle<'a>( + entry_points: &EntryPointMap, + server_transpiler: &'a mut Transpiler<'a>, + bake_options: bundle_v2::FrameworkBundleOptions<'a>, + alloc: &'a Arena, + event_loop: bundle_v2::EventLoop, +) -> bun_bundler::Result> { + let mut this = BundleV2::init( + server_transpiler, + Some(bake_options), + alloc, + event_loop, + false, + None, + alloc, + )?; + this.unique_key = bundle_v2::generate_unique_key(); + + // Wrap so every exit path hits the cleanup below; `chunks` must drop + // inside the closure, before `deinit_without_freeing_arena()`. + let result = (|| -> bun_bundler::Result> { + if this.transpiler.log().has_errors() { + return Err(bun_bundler::Error::BuildFailed); + } + + // Client files bundle for the browser; server files for the server + // transpiler's target. + let server_target = this.transpiler.options.target; + let mut entry_targets: Vec<(&[u8], bun_ast::Target)> = + Vec::with_capacity(entry_points.files.count()); + for key in entry_points.files.keys() { + entry_targets.push(( + key.abs_path(), + match key.side { + bake::Side::Client => bun_ast::Target::Browser, + bake::Side::Server => server_target, + }, + )); + } + this.enqueue_entry_points_with_targets(&entry_targets)?; + + if this.transpiler.log().has_errors() { + return Err(bun_bundler::Error::BuildFailed); + } + + this.wait_for_parse(); + + if this.transpiler.log().has_errors() { + return Err(bun_bundler::Error::BuildFailed); + } + + this.scan_for_secondary_paths(); + + this.process_server_component_manifest_files()?; + + let reachable_files = this.find_reachable_files()?; + + this.process_files_to_copy(&reachable_files)?; + + this.add_server_component_boundaries_as_extra_entry_points()?; + + this.clone_ast()?; + + // SAFETY: see `BundleV2::generate_from_cli` — raw-ptr borrow sidestep; + // `link` takes a raw `*mut BundleV2` and only touches fields disjoint + // from `this.linker`. + let mut chunks = unsafe { + let bundle_ptr: *mut BundleV2 = &raw mut *this; + let ep = (*bundle_ptr).graph.entry_points.as_slice(); + // Value-copy (original preserved for `StaticRouteVisitor`). + // Borrow — do NOT `take` (see `generate_from_cli`). + let scbs = &(*bundle_ptr).graph.server_component_boundaries; + // Project `.linker` via `bundle_ptr` so no second `Box::deref_mut` + // retag invalidates `ep`/`scbs` (SB hygiene). + (*bundle_ptr) + .linker + .link(bundle_ptr, ep, scbs, &reachable_files)? + }; + + if chunks.is_empty() { + return Ok(Vec::new()); + } + + bun_bundler::linker_context_mod::generate_chunks_in_parallel::( + &mut this.linker, + &mut chunks, + ) + })(); + + this.deinit_without_freeing_arena(); + + result +} + +/// `EntryPointMap.InputFile`. The `Hash`/`Eq` impls below are content-based +/// (not byte-layout) — store a +/// `RawSlice` and let `bun_ptr` encapsulate the unsafe re-borrow. +/// `RawSlice: Send + Sync`, so no manual auto-trait impls are needed. +#[derive(Copy, Clone)] +pub struct InputFile { + abs_path: bun_ptr::RawSlice, + pub side: bake::Side, +} +impl InputFile { + #[inline] + pub fn init(abs_path: &[u8], side: bake::Side) -> Self { + Self { + abs_path: bun_ptr::RawSlice::new(abs_path), + side, + } + } + #[inline] + pub fn abs_path(&self) -> &[u8] { + // Backing allocation is owned by `EntryPointMap.owned_paths` + // (duped on insert) and outlives every key stored in `files`. + self.abs_path.slice() + } +} +impl core::hash::Hash for InputFile { + fn hash(&self, state: &mut H) { + state.write(self.abs_path()); + state.write_u8(self.side as u8); + } +} +impl PartialEq for InputFile { + fn eq(&self, other: &Self) -> bool { + self.side == other.side && self.abs_path() == other.abs_path() + } +} +impl Eq for InputFile {} + +/// Value side is `OutputFile.Index` — left as a placeholder until the +/// bundle is indexed; the bundler never reads it. +pub(crate) type EntryPointHashMap = bun_collections::ArrayHashMap; + /// After a production bundle is generated, prerendering needs to be able to /// look up the generated chunks associated with each route's `OpaqueFileId` -/// This data structure contains that mapping, and is also used by bundle_v2 -/// to enqueue the entry points. -/// -/// Canonical definition lives in `bun_bundler::bake_types::production` (lower -/// tier) so the bundler and runtime share ONE nominal type. Re-exported here -/// for `bake::production::EntryPointMap` callers. -pub use bun_bundler::bake_types::production::EntryPointMap; -use bun_bundler::bake_types::production::{EntryPointHashMap, InputFile}; +/// This data structure contains that mapping, and is also the source of the +/// entry-point list handed to the bundler. +#[derive(Default)] +pub struct EntryPointMap { + pub root: Box<[u8]>, + /// `OpaqueFileId` is the insertion index into this map. + pub files: EntryPointHashMap, + /// Owned backing storage for the duped path bytes that `InputFile` + /// keys point into (raw ptr+len) — kept here so the allocations + /// drop with the map (no `Box::leak`). + pub owned_paths: Vec>, +} +impl EntryPointMap { + /// Mirrors `getOrPutEntryPoint`. Dupes `abs_path` on first insert + /// (owned by `owned_paths`; `Box` heap address is stable across the + /// move so the raw key pointer stays valid). + pub fn get_or_put_entry_point( + &mut self, + abs_path: &[u8], + side: bake::Side, + ) -> crate::Result { + let probe = InputFile::init(abs_path, side); + if let Some(index) = self.files.get_index(&probe) { + return Ok(OpaqueFileId::init(index as u32)); + } + let owned: Box<[u8]> = Box::<[u8]>::from(abs_path); + let key = InputFile::init(&owned, side); + self.owned_paths.push(owned); + let index = self.files.count(); + // Value is the post-bundle output index; left as a placeholder until + // the bundle is indexed. + self.files.put_no_clobber(key, OutputFileIndex::init(0))?; + Ok(OpaqueFileId::init(index as u32)) + } +} impl framework_router::InsertionHandler for EntryPointMap { fn get_file_id_for_router( @@ -1397,7 +1565,6 @@ impl framework_router::InsertionHandler for EntryPointMap { _: framework_router::FileKind, ) -> Result { self.get_or_put_entry_point(abs_path, bake::Side::Server) - .map(|id| OpaqueFileId::init(id.get())) .map_err(|_| bun_alloc::AllocError) } diff --git a/src/runtime/cli/Arguments.rs b/src/runtime/cli/Arguments.rs index ab0fc39e9b37..85efb6490e05 100644 --- a/src/runtime/cli/Arguments.rs +++ b/src/runtime/cli/Arguments.rs @@ -13,7 +13,7 @@ use bun_clap as clap; use bun_clap::parse_param; use bun_core::env::OperatingSystem; use bun_core::strings; -use bun_core::{self, FeatureFlags, Global, Output, env_var}; +use bun_core::{self, Global, Output, env_var}; use bun_jsc::RegularExpression; use bun_jsc::regular_expression::Flags as RegexFlags; use bun_options_types::code_coverage_options::Reporters as CoverageReporters; @@ -393,7 +393,7 @@ const BAKE_DEBUG_PARAMS: &[ParamType] = &[ ]; macro_rules! maybe_bake_debug_params { () => { - if FeatureFlags::BAKE_DEBUGGING_FEATURES { + if crate::bake::DEBUGGING_FEATURES { BAKE_DEBUG_PARAMS } else { &[] as &[ParamType] @@ -1636,7 +1636,7 @@ pub(crate) fn parse(cmd: CommandTag, ctx: Context<'_>) -> crate::Resultbun build v{}", bun_core::Global::package_json_version_with_sha @@ -2044,7 +2044,7 @@ fn parse_build_command_options( let production = args.flag(b"--production"); if args.flag(b"--app") { - if !FeatureFlags::bake() { + if !crate::bake::is_enabled() { Output::err_generic( "To use the experimental \"--app\" option, upgrade to the canary build of bun via \"bun upgrade --canary\"", (), @@ -2052,11 +2052,11 @@ fn parse_build_command_options( Global::crash(); } - ctx.bundler_options.bake = true; - ctx.bundler_options.bake_debug_dump_server = - FeatureFlags::BAKE_DEBUGGING_FEATURES && args.flag(b"--debug-dump-server-files"); - ctx.bundler_options.bake_debug_disable_minify = - FeatureFlags::BAKE_DEBUGGING_FEATURES && args.flag(b"--debug-no-minify"); + ctx.bundler_options.app = true; + ctx.bundler_options.debug_dump_server_files = + crate::bake::DEBUGGING_FEATURES && args.flag(b"--debug-dump-server-files"); + ctx.bundler_options.debug_no_minify = + crate::bake::DEBUGGING_FEATURES && args.flag(b"--debug-no-minify"); } if ctx.bundler_options.bytecode { @@ -2196,7 +2196,7 @@ fn parse_build_command_options( Global::exit(1); } - if ctx.bundler_options.bake { + if ctx.bundler_options.app { Output::err_generic( "target must be 'bun' when using --app. Received: {}", format_args!( diff --git a/src/runtime/cli/build_command.rs b/src/runtime/cli/build_command.rs index 92003b6f7c64..490682de9d05 100644 --- a/src/runtime/cli/build_command.rs +++ b/src/runtime/cli/build_command.rs @@ -73,7 +73,7 @@ impl BuildCommand { ctx.args.target = Some(api::Target::Bun); } - if ctx.bundler_options.bake { + if ctx.bundler_options.app { return crate::bake::production::build_command(ctx); } @@ -543,15 +543,9 @@ impl BuildCommand { )?; } - crate::bake::bake_body::add_import_meta_defines( + crate::bake::add_dev_server_components_defines( &mut this_transpiler.options.define, - crate::bake::Mode::Development, - crate::bake::Side::Server, - )?; - crate::bake::bake_body::add_import_meta_defines( &mut ct.options.define, - crate::bake::Mode::Development, - crate::bake::Side::Client, )?; this_transpiler.sync_resolver_opts(); diff --git a/src/jsc/generated_classes_list.rs b/src/runtime/generated_classes_list.rs similarity index 88% rename from src/jsc/generated_classes_list.rs rename to src/runtime/generated_classes_list.rs index 0a3be533a841..63c2e5a71b4b 100644 --- a/src/jsc/generated_classes_list.rs +++ b/src/runtime/generated_classes_list.rs @@ -1,20 +1,16 @@ //! LAYERING: `Classes` is a flat namespace of //! `pub use` aliases mapping each `.classes.ts` class name to -//! its native backing type. Every target lives under `bun.api`, `bun.webcore`, -//! `bun.bake`, or `bun.SourceMap` — i.e. in the Rust crate graph, in -//! `bun_runtime` / `bun_sql_jsc` / `bun_sourcemap_jsc`, all of which **depend -//! on** `bun_jsc`. Re-exporting them from `bun_jsc` would create a hard cycle. +//! its native backing type. Every target lives in `bun_runtime` / +//! `bun_sql_jsc` / `bun_sourcemap_jsc`, all of which **depend on** `bun_jsc` — +//! so this list lives here, not in `bun_jsc`, where re-exporting them would +//! create a hard cycle. The public name is `bun_runtime::GeneratedClassesList`; +//! `bun_jsc::GeneratedClassesList` is intentionally absent. //! //! The codegen output //! (`generated_classes.rs`) does **not** consume this list — it resolves each //! class to its Rust struct via `rustModuleResolver.resolveStruct` //! (`generate-classes.ts:2602`/`:3450`) and is `include!`d into `bun_runtime` //! where every backing type is already in scope. -//! -//! Resolution: this file is `#[path]`-mounted from **`bun_runtime/lib.rs`** -//! (not `bun_jsc/lib.rs`) so every alias resolves via `crate::`. The public -//! name is `bun_runtime::GeneratedClassesList`; `bun_jsc::GeneratedClassesList` -//! is intentionally absent. #[allow(non_snake_case, unused_imports)] pub mod Classes { @@ -61,7 +57,6 @@ pub mod Classes { pub use crate::api::js_bundler::BuildArtifact; pub use crate::api::js_bundler::JSBundler as Bundler; pub use crate::api::js_transpiler as Transpiler; - pub use crate::bake::framework_router::JSFrameworkRouter as FrameworkFileSystemRouter; pub(crate) use crate::crypto::MD4; pub(crate) use crate::crypto::MD5; pub(crate) use crate::crypto::SHA1; @@ -72,6 +67,9 @@ pub mod Classes { pub(crate) use crate::crypto::SHA512_256; pub use crate::dns_jsc::Resolver as DNSResolver; pub use crate::ffi::FFI; + // Declared in `bake/FrameworkRouter.classes.ts` with an explicit + // `rustPath`; the alias is owned by the codegen output. + pub use crate::generated_classes::FrameworkFileSystemRouter; pub use crate::node::net::block_list as BlockList; pub use crate::node::node_fs_binding::Binding as NodeJSFS; pub use crate::node::node_fs_stat_watcher::StatWatcher; diff --git a/src/runtime/lib.rs b/src/runtime/lib.rs index b07914cca95f..56e96ff4f4ca 100644 --- a/src/runtime/lib.rs +++ b/src/runtime/lib.rs @@ -38,6 +38,7 @@ pub mod shell; #[path = "api.rs"] pub mod api; pub mod dispatch; +pub mod generated_classes_list; pub mod hw_exports; pub mod ipc; pub mod ipc_host; @@ -49,11 +50,6 @@ pub mod napi; #[path = "../bun.js.rs"] pub mod run_main; pub mod timer; -// `generated_classes_list.rs` lives under `src/jsc/` but every type it -// aliases is defined in this crate (api/webcore/test_runner/bake) or a -// same-tier dep, so it is `#[path]`-mounted here to avoid a bun_jsc cycle. -#[path = "../jsc/generated_classes_list.rs"] -pub mod generated_classes_list; pub use generated_classes_list::Classes as GeneratedClassesList; pub mod generated_classes; // include!()s ${BUN_CODEGEN_DIR}/generated_classes.rs pub mod generated_host_exports; // include!()s ${BUN_CODEGEN_DIR}/generated_host_exports.rs diff --git a/src/runtime/server/AnyRequestContext.rs b/src/runtime/server/AnyRequestContext.rs index e50a24ca256e..8a9753268eb5 100644 --- a/src/runtime/server/AnyRequestContext.rs +++ b/src/runtime/server/AnyRequestContext.rs @@ -9,7 +9,7 @@ use crate::webcore::CookieMap; pub use super::request_context::AdditionalOnAbortCallback; use super::request_context::RequestContext; -use super::{DebugHTTPSServer, DebugHTTPServer, HTTPSServer, HTTPServer}; +use super::{DebugHTTPSServer, DebugHTTPServer, DevServerSlot, HTTPSServer, HTTPServer}; // The six monomorphizations of `NewRequestContext` (ssl × debug × h3). type HttpCtx = RequestContext; @@ -221,28 +221,18 @@ impl AnyRequestContext { dispatch!(self, (), |_T, ctx| ctx.set_signal_aborted(reason)) } - pub(crate) fn dev_server(self) -> Option<&'static crate::bake::DevServer::DevServer> { - dispatch!(self, None, |_T, ctx| ctx.dev_server().map(|r| { - // SAFETY: the server backref outlives any AnyRequestContext (held only - // for the duration of a request callback); `self` is a by-value tagged - // pointer, so there is no input lifetime to tie the borrow to. - unsafe { bun_ptr::detach_lifetime_ref(r) } - })) - } - - /// Mutable access to the attached DevServer. The accessor above hands out - /// `&` only. The `Box` slot - /// inside `NewServer` has a stable address, so deriving `&mut` here is - /// sound as long as the caller upholds the usual single-writer rule on the - /// JS thread. - pub(crate) fn dev_server_mut(self) -> Option<*mut crate::bake::DevServer::DevServer> { + /// Erased pointer to the dev server attached to this context's server + /// (`None` when none is attached or the context is detached). Only the + /// dev-server module knows the concrete type behind the slot; the typed + /// `dev_server()`/`dev_server_mut()` views over this accessor are defined + /// there, next to the slot's `Deref` impls. + pub(crate) fn dev_server_ptr(self) -> Option> { dispatch!(self, None, |_T, ctx| { let server = ctx.server.get()?.as_ptr(); - // SAFETY: `ctx.server` is a non-null backref that outlives this context - // and `dev_server` is a `Box` field never moved while requests are in - // flight, so dereferencing for exclusive access on the JS thread is sound. - let ds = unsafe { (*server).dev_server.as_deref_mut()? }; - Some(core::ptr::from_mut(ds)) + // SAFETY: `ctx.server` is a non-null backref that outlives this + // context; the slot's pointee is a stable heap allocation never + // moved while requests are in flight. + unsafe { (*server).dev_server.as_ref().map(DevServerSlot::as_ptr) } }) } diff --git a/src/runtime/server/HTMLBundle.rs b/src/runtime/server/HTMLBundle.rs index 73e22881373d..d66a03f39c49 100644 --- a/src/runtime/server/HTMLBundle.rs +++ b/src/runtime/server/HTMLBundle.rs @@ -22,7 +22,6 @@ use crate::api::js_bundle_completion_task::{ }; use crate::api::js_bundler::js_bundler::{self as JSBundler, Config as JSBundlerConfig}; use crate::api::output_file_jsc::OutputFileJsc as _; -use crate::bake::dev_server::route_bundle; use crate::server::jsc::{JSGlobalObject, JSValue, JsResult}; use crate::server::server_config::MethodOptional; use crate::server::{AnyRoute, AnyServer, GetOrStartLoadResult, ServePluginsCallback, StaticRoute}; @@ -135,6 +134,15 @@ impl HTMLBundle { /// Deprecated: use Route instead. pub(crate) type HTMLBundleRoute = Route; +/// Marker for [`DevServerRouteId`]. +pub enum DevServerRouteIdMarker {} + +/// Opaque per-route token assigned by an attached dev server (see the +/// dev-server slot seam in `mod.rs`) when it registers the route with its +/// bundler. The host only reserves the [`Route::dev_server_id`] slot; the +/// dev-server module defines what the token means. +pub type DevServerRouteId = bun_core::GenericIndex; + /// An HTMLBundle can be used across multiple server instances, an /// HTMLBundle.Route can only be used on one server, but is also /// reference-counted because a server can have multiple instances of the same @@ -158,9 +166,9 @@ pub struct Route { pub(crate) server: Cell>, /// When using DevServer, this value is never read or written to. pub(crate) state: JsCell, - /// Written and read by DevServer to identify if this route has been - /// registered with the bundler. - pub(crate) dev_server_id: Cell>, + /// Written and read by the attached dev server to identify if this route + /// has been registered with its bundler. + pub(crate) dev_server_id: Cell>, /// When state == .pending, incomplete responses are stored here. // Raw `*mut` because the pointer is handed to uws onAborted callback and // compared by identity; allocation/free is via heap::alloc/from_raw. diff --git a/src/runtime/server/RequestContext.rs b/src/runtime/server/RequestContext.rs index c31414ac1a2a..b7dbe990844f 100644 --- a/src/runtime/server/RequestContext.rs +++ b/src/runtime/server/RequestContext.rs @@ -219,12 +219,6 @@ where pub(crate) fn is_async(&self) -> bool { self.defer_deinit_until_callback_completes.get().is_none() } - - pub(crate) fn dev_server(&self) -> Option<&crate::bake::DevServer::DevServer> { - let server = self.server.get()?; - // SAFETY: BACKREF — the server outlives every context it allocates. - unsafe { &*server.as_ptr() }.dev_server() - } } // ─── per-request state machine bodies ──────────────────────────────────────── diff --git a/src/runtime/server/ServerConfig.rs b/src/runtime/server/ServerConfig.rs index 5e495dfbbe6b..c1a692c6b106 100644 --- a/src/runtime/server/ServerConfig.rs +++ b/src/runtime/server/ServerConfig.rs @@ -69,7 +69,13 @@ pub struct ServerConfig { pub(crate) negative_routes: Vec, pub(crate) user_routes_to_build: Vec, - pub(crate) bake: Option, + /// Dev-server options parsed from the `app` option or derived from HTML + /// imports / framework routers in `routes`. Presence causes `Bun.serve` + /// to initialize the dev server at listen time. The erased handle is the + /// options half of the dev-server seam defined next to + /// [`super::DevServerSlot`] in `mod.rs`; the concrete type is private to + /// the dev-server module. + pub(crate) dev_server_options: Option, } impl Default for ServerConfig { @@ -100,7 +106,7 @@ impl Default for ServerConfig { static_routes: Vec::new(), negative_routes: Vec::new(), user_routes_to_build: Vec::new(), - bake: None, + dev_server_options: None, } } } @@ -133,7 +139,7 @@ pub enum DevelopmentOption { } impl DevelopmentOption { - fn is_hmr_enabled(self) -> bool { + pub(crate) fn is_hmr_enabled(self) -> bool { self == DevelopmentOption::Development } @@ -287,7 +293,7 @@ impl ServerConfig { static_routes: core::mem::take(&mut self.static_routes), negative_routes: core::mem::take(&mut self.negative_routes), user_routes_to_build: core::mem::take(&mut self.user_routes_to_build), - bake: self.bake.take(), + dev_server_options: self.dev_server_options.take(), }; that.normalize_static_routes_list()?; @@ -673,46 +679,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 fn from_js( global: &JSGlobalObject, @@ -863,17 +829,16 @@ 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, + js_string_allocations: Default::default(), 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. + // Fields are owned locals; drop on `?` automatically. The router + // list and string allocations transfer to args.dev_server_options + // on the success path via the from_serve_routes hook below. // (dedupe_html_bundle_map is unused on the success path; drops at scope end.) // Vec drops elements (which deref route) @@ -1046,86 +1011,10 @@ impl ServerConfig { // When HTML bundles are provided, ensure DevServer options are ready // The presence of these options causes Bun.serve to initialize things. - if !init_ctx.dedupe_html_bundle_map.is_empty() - || !init_ctx.framework_router_list.is_empty() + if let Some(options) = + super::__bun_bake_dev_server_options_from_serve_routes(init_ctx, args.development)? { - 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 mut user_options = crate::bake::UserOptions { - arena, - allocations: core::mem::replace( - &mut init_ctx.js_string_allocations, - crate::bake::StringRefList::EMPTY, - ), - root, - framework, - bundler_options: bb::SplitBundlerOptions::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 = DotEnvBehavior::prefix; - } - DotEnvBehavior::load_all => { - user_options.bundler_options.client.env = DotEnvBehavior::load_all; - } - DotEnvBehavior::disable => { - user_options.bundler_options.client.env = DotEnvBehavior::disable; - } - _ => {} - } - - if let Some(define) = &o.serve_define { - user_options.bundler_options.client.define = define.clone(); - user_options.bundler_options.server.define = define.clone(); - user_options.bundler_options.ssr.define = define.clone(); - } - - args.bake = Some(user_options); - } else { - if !init_ctx.framework_router_list.is_empty() { - return Err(global.throw_invalid_arguments(format_args!( - "FrameworkRouter is currently only supported when `development: true`", - ))); - } - } + args.dev_server_options = Some(options); } } @@ -1275,27 +1164,14 @@ impl ServerConfig { return Err(JsError::Thrown); } - if opts.allow_bake_config { - 'brk: { - if let Some(bake_args_js) = arg.get_truthy(global, "app")? { - if !bun_core::FeatureFlags::bake() { - break 'brk; - } - if args.bake.is_some() { - // "app" is likely to be removed in favor of the HTML loader. - return Err(global.throw_invalid_arguments(format_args!( - "'app' + HTML loader not supported.", - ))); - } - - if args.development == DevelopmentOption::Production { - return Err(global.throw_invalid_arguments(format_args!( - "TODO: 'development: false' in serve options with 'app'. For now, use `bun build --app` or set 'development: true'", - ))); - } - - args.bake = Some(crate::bake::UserOptions::from_js(bake_args_js, global)?); - } + if opts.allow_dev_server_options { + if let Some(options) = super::__bun_bake_dev_server_options_from_app( + arg, + global, + args.dev_server_options.is_some(), + args.development, + )? { + args.dev_server_options = Some(options); } } @@ -1368,7 +1244,7 @@ impl ServerConfig { .throw_invalid_arguments(format_args!("Expected fetch() to be a function"))); } args.on_request = on_request_; - } else if args.bake.is_none() + } else if args.dev_server_options.is_none() && !args.is_node_http_server && ((args.static_routes.len() + args.user_routes_to_build.len()) == 0 && !opts.has_user_routes) @@ -1638,7 +1514,7 @@ impl ServerConfig { // NOTE: deferred assertion from top of fn if !args.development.is_hmr_enabled() { - debug_assert!(args.bake.is_none()); + debug_assert!(args.dev_server_options.is_none()); } Ok(args) @@ -1647,7 +1523,11 @@ impl ServerConfig { #[derive(Clone, Copy)] pub struct FromJSOptions { - pub(crate) allow_bake_config: bool, + /// Whether this entry point may consult the dev-server option in the + /// serve config (`Bun.serve` may; `server.reload` may not). Feature + /// gating on top of this is owned by + /// [`super::__bun_bake_dev_server_options_from_app`]. + pub(crate) allow_dev_server_options: bool, pub(crate) is_fetch_required: bool, pub(crate) has_user_routes: bool, } @@ -1655,7 +1535,7 @@ pub struct FromJSOptions { impl Default for FromJSOptions { fn default() -> Self { Self { - allow_bake_config: true, + allow_dev_server_options: true, is_fetch_required: true, has_user_routes: false, } diff --git a/src/runtime/server/mod.rs b/src/runtime/server/mod.rs index 045509a0f3cc..144fe51503ae 100644 --- a/src/runtime/server/mod.rs +++ b/src/runtime/server/mod.rs @@ -115,7 +115,8 @@ pub use any_request_context::AnyRequestContext; #[path = "server_body.rs"] mod server_body; pub use server_body::{ - BunInfo, GetOrStartLoadResult, PreparedRequestFor, ServePluginsCallback, ServerInitContext, + BunInfo, FrameworkRouterSeam, FrameworkRouterTypes, GetOrStartLoadResult, PreparedRequestFor, + ServePluginsCallback, ServePluginsConsumer, ServerInitContext, }; // ─── write_status ──────────────────────────────────────────────────────────── @@ -147,6 +148,20 @@ pub(crate) fn write_status(resp: *mut uws_sys::NewAppResponse` is unsuitable (would add // a second header and break the round-trip). Hold them as raw intrusive // pointers. +/// Compile-time seam, sibling of [`FrameworkRouterTypes`] (which projects the +/// route-parsing collection state): the dev-server module supplies the +/// concrete payload type of [`AnyRoute::FrameworkRouter`] by implementing +/// this on [`FrameworkRouterSeam`]. The host never reads the payload — it is +/// produced by the dev-server route parser +/// (`ServerInitContext::framework_router_from_js`) and consumed by the dev +/// server's own route registration — so a dev-server rewrite can swap the +/// type without touching this file. +pub trait FrameworkRouterRouteTypes { + /// Index of the parsed `{ dir, style }` mount within the dev server's + /// framework router. + type TypeIndex: Copy; +} + pub enum AnyRoute { /// Serve a static file — `"/robots.txt": new Response(...)` Static(core::ptr::NonNull), @@ -157,7 +172,7 @@ pub enum AnyRoute { /// Bundle an HTML import — `import html from "./index.html"; "/": html` Html(bun_ptr::RefPtr), /// Use file-system routing — `"/*": { dir: …, style: "nextjs-pages" }` - FrameworkRouter(crate::bake::framework_router::TypeIndex), + FrameworkRouter(::TypeIndex), } impl AnyRoute { @@ -174,7 +189,9 @@ impl AnyRoute { AnyRoute::Directory(p) => bun_ptr::BackRef::from(*p).memory_cost(), AnyRoute::Html(r) => r.data().memory_cost(), AnyRoute::FrameworkRouter(_) => { - core::mem::size_of::() + // One parsed mount's worth of router-type config (the same + // projected type the route parser collects per entry). + core::mem::size_of::<::Mount>() } } } @@ -224,6 +241,207 @@ bitflags::bitflags! { } } +// ─── Dev-server seam ───────────────────────────────────────────────────────── +// The dev-server module owns the concrete types behind this seam; the server +// host only stores erased handles and calls through the vtable below. Both +// halves of the seam are defined here: +// +// * options — `ServerConfig::from_js` turns the `app` serve option / the +// HTML imports and framework routers in `routes` into an erased +// [`DevServerOptions`] (via the two `__bun_bake_dev_server_options_*` +// hooks) and stores it on `ServerConfig::dev_server_options`. +// * instance — listen time turns a config that carried options into an +// erased [`DevServerSlot`] (via `__bun_dev_server_from_server_config`). +// +// Construction goes through the definer-prefixed link-time hooks below (same +// pattern as the bundler's `__bun_bake_convert_stmts_for_chunk_hmr` hook in +// `bun_bundler::lib.rs`), so a rewrite of the dev server never touches the +// server host. Request paths that still need the concrete type (the +// `as_deref` callers in the per-request files) go through the +// `Deref`/`DerefMut` impls defined next to the dev server. + +/// Owned, type-erased dev-server options: the parsed `app` config, or the +/// options derived from HTML imports / framework routers in `routes`. +/// Presence on `ServerConfig::dev_server_options` causes `Bun.serve` to +/// initialize the dev server at listen time. +pub struct DevServerOptions { + ptr: core::ptr::NonNull<()>, + drop_fn: unsafe fn(core::ptr::NonNull<()>), +} + +impl DevServerOptions { + /// # Safety + /// Reserved for the `__bun_bake_dev_server_options_*` hook bodies: `ptr` + /// must own the boxed value their paired downcast expects, and `drop_fn` + /// must free exactly that value. Ownership of `ptr` transfers to the + /// handle, which calls `drop_fn` once on drop. + pub(crate) unsafe fn from_raw( + ptr: core::ptr::NonNull<()>, + drop_fn: unsafe fn(core::ptr::NonNull<()>), + ) -> Self { + Self { ptr, drop_fn } + } + + /// The erased pointer. Only the constructing module knows the concrete + /// type behind it. + pub(crate) fn as_ptr(&self) -> core::ptr::NonNull<()> { + self.ptr + } +} + +impl Drop for DevServerOptions { + fn drop(&mut self) { + // SAFETY: `from_raw`'s contract — the handle solely owns `ptr` and + // `drop_fn` pairs with its allocation. + unsafe { (self.drop_fn)(self.ptr) } + } +} + +/// Calls the host makes into an attached dev server. The dev-server module +/// supplies the single static instance whose bodies downcast `ptr`; +/// [`DevServerSlot::from_raw`] pairs the pointer with that vtable. +pub struct DevServerSlotVTable { + /// Drops the boxed dev server (its deinit). + pub(crate) drop_fn: unsafe fn(core::ptr::NonNull<()>), + /// Heap bytes retained by the dev server (for `Server.memoryCost`). + pub(crate) memory_cost: fn(core::ptr::NonNull<()>) -> usize, + /// Mirrors the host's inspector/debugger id into the dev server (its + /// HMR agent tags inspector events with it). + pub(crate) set_inspector_server_id: fn(core::ptr::NonNull<()>, jsc::DebuggerId), + /// DNS-rebinding guard: whether the request's `Host` header names an + /// origin allowed to reach dev-server-internal routes (loopback names, + /// IP literals, or the configured hostname). + pub(crate) is_allowed_host: fn(core::ptr::NonNull<()>, &uws::Request) -> bool, + /// Routes an `Html` static route's bundle through the dev server (which + /// serves it with HMR) instead of the prebundled static handler. + pub(crate) put_html_route: fn( + core::ptr::NonNull<()>, + path: &[u8], + route: *mut html_bundle::Route, + ) -> crate::Result<()>, + /// Registers the dev server's own routes on the server's uWS app. + /// Returns true if a catch-all "/*" handler was attached. + pub(crate) set_routes: fn(core::ptr::NonNull<()>, AnyServer) -> crate::Result, +} + +/// Owned, type-erased dev server attached to a [`NewServer`]. Dropping the +/// slot drops the dev server. +pub struct DevServerSlot { + raw: DevServerSlotRaw, +} + +/// Copyable erased view over the dev server (`(ptr, vtable)`, no ownership), +/// for `set_routes`' borrowck/aliasing reshape: the pointee is a stable heap +/// allocation owned by the slot, so a copy taken from `&mut DevServerSlot` +/// stays valid while `NewServer.dev_server` is untouched — without keeping a +/// borrow of the server alive across calls that re-derive `&mut NewServer` +/// (the dev server's own route registration does). +#[derive(Clone, Copy)] +pub struct DevServerSlotRaw { + ptr: core::ptr::NonNull<()>, + vtable: &'static DevServerSlotVTable, +} + +impl DevServerSlot { + /// # Safety + /// Reserved for the `__bun_dev_server_from_server_config` hook body: + /// `ptr` must own the boxed dev server its `vtable` bodies downcast to, + /// and `vtable.drop_fn` must free exactly that value. Ownership of `ptr` + /// transfers to the slot, which calls `drop_fn` once on drop. + pub(crate) unsafe fn from_raw( + ptr: core::ptr::NonNull<()>, + vtable: &'static DevServerSlotVTable, + ) -> Self { + Self { + raw: DevServerSlotRaw { ptr, vtable }, + } + } + + /// The erased pointer. Only the dev-server module knows the concrete type + /// behind it (its `Deref` impls on this slot are the downcast). + pub(crate) fn as_ptr(&self) -> core::ptr::NonNull<()> { + self.raw.ptr + } + + pub fn memory_cost(&self) -> usize { + (self.raw.vtable.memory_cost)(self.raw.ptr) + } + + pub fn set_inspector_server_id(&mut self, id: jsc::DebuggerId) { + (self.raw.vtable.set_inspector_server_id)(self.raw.ptr, id) + } + + /// See [`DevServerSlotVTable::is_allowed_host`]. + pub(crate) fn is_allowed_host(&self, req: &uws::Request) -> bool { + (self.raw.vtable.is_allowed_host)(self.raw.ptr, req) + } + + /// See [`DevServerSlotRaw`]. Takes `&mut self` so the copy inherits the + /// caller's exclusive claim on the dev server. + pub(crate) fn raw(&mut self) -> DevServerSlotRaw { + self.raw + } +} + +impl Drop for DevServerSlot { + fn drop(&mut self) { + // SAFETY: `from_raw`'s contract — the slot solely owns `ptr` and + // `drop_fn` pairs with its allocation. + unsafe { (self.raw.vtable.drop_fn)(self.raw.ptr) } + } +} + +impl DevServerSlotRaw { + pub(crate) fn put_html_route( + self, + path: &[u8], + route: *mut html_bundle::Route, + ) -> crate::Result<()> { + (self.vtable.put_html_route)(self.ptr, path, route) + } + + pub(crate) fn set_routes(self, server: AnyServer) -> crate::Result { + (self.vtable.set_routes)(self.ptr, server) + } +} + +unsafe extern "Rust" { + /// Defined `#[no_mangle]` in the dev-server module (`bake/bake_body.rs`). + /// Derives dev-server options from the HTML bundles and framework routers + /// collected while parsing `routes`. `Ok(None)` when the routes need no + /// dev server; errors when framework routers are present without + /// `development: true`. All arguments are safe Rust types (no raw-pointer + /// preconditions), so the link-time-resolved body upholds Rust's + /// invariants on its own. + pub(crate) safe fn __bun_bake_dev_server_options_from_serve_routes( + init_ctx: &mut ServerInitContext<'_>, + development: server_config::DevelopmentOption, + ) -> JsResult>; + + /// Defined `#[no_mangle]` in the dev-server module (`bake/bake_body.rs`). + /// Reads and parses the dev-server option from the full serve options + /// object — the hook owns the property name and its feature gating. + /// `Ok(None)` when the dev-server feature flag is disabled (without + /// touching the object, so no user getter runs) or the option is absent; + /// errors when options were already derived from `routes` + /// (`has_existing_options`) or `development` is `Production`. + pub(crate) safe fn __bun_bake_dev_server_options_from_app( + serve_options: JSValue, + global: &JSGlobalObject, + has_existing_options: bool, + development: server_config::DevelopmentOption, + ) -> JsResult>; + + /// Defined `#[no_mangle]` in the dev-server module (`bake/DevServer.rs`). + /// Builds the dev server for a config that carried `dev_server_options` + /// (`Ok(None)` when it didn't). All arguments are safe Rust types (no + /// raw-pointer preconditions), so the link-time-resolved body upholds + /// Rust's invariants on its own. + safe fn __bun_dev_server_from_server_config( + config: &mut ServerConfig, + ) -> JsResult>; +} + // ─── NewServer ─────────────────────────────────────────────────────────────── /// Number of HTTP method tokens — must match the variant count of /// `bun_http_types::Method::Method` (`ACL`..`UNSUBSCRIBE`). Sizes @@ -308,7 +526,9 @@ pub struct NewServer { /// counted ref held here is released in `Drop for NewServer`. pub(crate) plugins: Option>, - pub(crate) dev_server: Option>, + /// The attached dev server (HMR / HTML imports in development), + /// type-erased behind the slot seam above. + pub(crate) dev_server: Option, /// Route → index in RouteList.cpp. User routes may be applied multiple /// times due to SNI, so we have to store them. @@ -364,11 +584,15 @@ fn any_response_from(resp: *mut uws_sys::NewAppResponse) - pub type ServerRequestContext = request_context::RequestContext, SSL, DEBUG, false>; -#[derive(Clone, Copy, PartialEq, Eq)] +#[derive(Clone, Copy)] pub enum CreateJsRequest { Yes, No, - Bake, + /// Create the JS wrapper via the supplied materializer instead of + /// `Request::to_js` — for callers whose handlers need a wrapper class + /// carrying extra per-route state. `Err(OutOfMemory)` is routed to the + /// OOM handler; any other error aborts request preparation. + Custom(fn(&crate::webcore::Request, &JSGlobalObject) -> JsResult), } /// Bundle of the JS-side `Request`, the heap @@ -828,7 +1052,8 @@ impl NewServer { let signal_ref = unsafe { jsc::AbortSignalRef::adopt(bun_opaque::opaque_deref_mut(signal).ref_()) }; // ownership: `Request::new` is `bun.TrivialNew` — the heap - // allocation is handed to the JS GC via `to_js`/`to_js_for_bake` (C++ + // allocation is handed to the JS GC via `to_js`/the `Custom` + // materializer (C++ // wrapper finalizer frees it), or, for `CreateJsRequest::No`, retained // by `ctx.request_weakref` until `RequestContext::deinit` releases it. // `body_hive` (the original +1) moves into the Request — paired drop in @@ -908,10 +1133,10 @@ impl NewServer { // SAFETY: `request_object` is the freshly-allocated heap // `Request`; ownership transfers to the JS wrapper. CreateJsRequest::Yes => unsafe { (*request_object).to_js(global) }, - CreateJsRequest::Bake => { + CreateJsRequest::Custom(materialize) => { // SAFETY: `request_object` is the freshly-allocated heap // `Request`; ownership transfers to the JS wrapper. - match unsafe { (*request_object).to_js_for_bake(global) } { + match materialize(unsafe { &*request_object }, global) { Ok(v) => v, Err(jsc::JsError::OutOfMemory) => bun_core::out_of_memory(), Err(_) => return None, @@ -924,8 +1149,10 @@ impl NewServer { }) } - /// Invoke the user's route handler for a - /// request that was deferred (bake bundle-then-serve flow). + /// Invoke the user's route handler for a request that was deferred (the + /// caller's bundle-then-serve flow). `create_js_request` selects how the + /// JS `Request` is materialized when `req` is still a stack request; + /// already-saved requests carry the JS value they were saved with. /// /// # Safety /// `this` must point to a live heap-allocated `NewServer`; `resp` must be @@ -936,6 +1163,7 @@ impl NewServer { resp: *mut uws_sys::NewAppResponse, callback: JSValue, extra_args: [JSValue; ARG_COUNT], + create_js_request: CreateJsRequest, ) { // Same gate as the network trampolines: the saved request's // `pending_requests` increment keeps the wrapper `Strong` (so it is @@ -957,7 +1185,7 @@ impl NewServer { bun_opaque::opaque_deref_mut(r), resp, None, - CreateJsRequest::Bake, + create_js_request, None, ) { Some(p) => p, @@ -2201,42 +2429,20 @@ 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 - // `(*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 - // (since-moved) stack slot. On Err, the `Box` drop frees the - // half-built server. + // The dev-server options (and the arena that backs its root) live in + // `(*server).config` for the server's lifetime. Initialise the dev + // server AFTER the server box exists so the arena borrow points into + // the heap-allocated config rather than the caller's (since-moved) + // stack slot. On Err, the `Box` drop frees the half-built server. // SAFETY: `server` is the freshly-boxed `*mut Self`; uniquely owned here. - if let Some(bake_options) = unsafe { &mut (*server).config.bake } { - // SAFETY: `server` is the freshly-boxed `*mut Self`; uniquely owned here. - let broadcast = unsafe { - (*server) - .config - .broadcast_console_log_from_browser_to_server_for_bake - }; - let dev = match crate::bake::DevServer::init(crate::bake::DevServer::Options { - arena: &bake_options.arena, - root: bake_options.root, - // 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(), - broadcast_console_log_from_browser_to_server: broadcast, - }) { - Ok(d) => d, - Err(e) => { - // SAFETY: paired with heap::alloc above. - drop(unsafe { bun_core::heap::take(server) }); - return Err(e); - } - }; + match __bun_dev_server_from_server_config(unsafe { &mut (*server).config }) { // SAFETY: `server` is uniquely owned here. - unsafe { (*server).dev_server = Some(dev) }; + Ok(dev) => unsafe { (*server).dev_server = dev }, + Err(e) => { + // SAFETY: paired with heap::alloc above. + drop(unsafe { bun_core::heap::take(server) }); + return Err(e); + } } if SSL { @@ -2260,11 +2466,11 @@ impl NewServer { let app = bun_opaque::opaque_deref_mut(self.app.unwrap()); let self_ptr: *mut Self = self; let any_server = AnyServer::from(self_ptr.cast_const()); - // reshaped for borrowck — `dev_server` is `Option>`; - // snapshot the raw `*mut DevServer` so per-iteration `&mut` derives - // don't conflict with `&mut self.config` / `&mut self.user_routes`. - let dev_server: Option<*mut crate::bake::DevServer::DevServer> = - self.dev_server.as_deref_mut().map(std::ptr::from_mut); + // reshaped for borrowck/aliasing — snapshot the copyable erased view + // (see `DevServerSlotRaw`) so per-iteration calls don't conflict with + // `&mut self.config` / `&mut self.user_routes`, and so no borrow of + // `self` is live when the dev server re-derives `&mut NewServer`. + let dev_server: Option = self.dev_server.as_mut().map(DevServerSlot::raw); // https://chromium.googlesource.com/devtools/devtools-frontend/+/main/docs/ecosystem/automatic_workspace_folders.md // Only enable this when we're using the dev server. @@ -2574,14 +2780,7 @@ impl NewServer { } } if let Some(dev) = dev_server { - // SAFETY: `dev` is the live `*mut DevServer` snapshotted - // from `self.dev_server` above; no other `&mut` to it - // is live in this loop. - bun_core::handle_oom( - unsafe { &mut *dev } - .html_router - .put(&entry.path, r.as_ptr()), - ); + bun_core::handle_oom(dev.put_html_route(&entry.path, r.as_ptr())); } needs_plugins = true; } @@ -2627,13 +2826,11 @@ impl NewServer { // --- 8. Handle DevServer routes & track "/*" coverage --- let mut has_dev_server_for_star_path = false; if let Some(dev) = dev_server { - // dev.setRoutes might register its own "/*" HTTP handler - // SAFETY: `dev` is the live `*mut DevServer` snapshotted from - // `self.dev_server` above; `self_ptr` is the live server. The two - // allocations are disjoint so the `&mut` borrows do not alias. - has_dev_server_for_star_path = bun_core::handle_oom( - unsafe { &mut *dev }.set_routes::(unsafe { &mut *self_ptr }), - ); + // dev set_routes might register its own "/*" HTTP handler. It + // re-derives `&mut NewServer` from `any_server`; the erased view + // holds no borrow of `self`, and the dev-server allocation is + // disjoint from the server's, so the `&mut`s do not alias. + has_dev_server_for_star_path = bun_core::handle_oom(dev.set_routes(any_server)); if has_dev_server_for_star_path { // Assume dev server "/*" covers all methods if it exists star_methods_covered_by_user = http_method::Set::all(); @@ -3551,7 +3748,9 @@ pub trait ServerLike { fn vm_mut(&self) -> *mut jsc::VirtualMachine; fn config(&self) -> &ServerConfig; fn on_request_complete(&mut self); - fn dev_server(&self) -> Option<&crate::bake::DevServer::DevServer>; + /// The (erased) `dev_server` slot, when one is attached. Typed views over + /// the slot are defined next to the dev server (its `Deref` impls). + fn dev_server(&self) -> Option<&DevServerSlot>; fn js_value(&self) -> &jsc::JsRef; fn h3_alt_svc(&self) -> Option<&[u8]>; fn terminated(&self) -> bool; @@ -3591,8 +3790,8 @@ impl ServerLike for NewServer { Self::on_request_complete(self) } #[inline] - fn dev_server(&self) -> Option<&crate::bake::DevServer::DevServer> { - self.dev_server.as_deref() + fn dev_server(&self) -> Option<&DevServerSlot> { + self.dev_server.as_ref() } #[inline(always)] fn js_value(&self) -> &jsc::JsRef { @@ -3672,7 +3871,7 @@ impl AnyServer { // `unsafe` with the existing caller-upheld exclusivity contract. #[inline(always)] - fn as_http(&self) -> &HTTPServer { + pub(crate) fn as_http(&self) -> &HTTPServer { debug_assert!(matches!(self.tag, AnyServerTag::HTTPServer)); // SAFETY: `ptr` was produced by `AnyServer::from::` and // is non-null while the server is alive (heap-allocated `NewServer`, @@ -3681,21 +3880,21 @@ impl AnyServer { } #[inline(always)] - fn as_https(&self) -> &HTTPSServer { + pub(crate) fn as_https(&self) -> &HTTPSServer { debug_assert!(matches!(self.tag, AnyServerTag::HTTPSServer)); // SAFETY: tag-matched non-null `NewServer`; see `as_http`. unsafe { &*self.ptr.cast::() } } #[inline(always)] - fn as_debug_http(&self) -> &DebugHTTPServer { + pub(crate) fn as_debug_http(&self) -> &DebugHTTPServer { debug_assert!(matches!(self.tag, AnyServerTag::DebugHTTPServer)); // SAFETY: tag-matched non-null `NewServer`; see `as_http`. unsafe { &*self.ptr.cast::() } } #[inline(always)] - fn as_debug_https(&self) -> &DebugHTTPSServer { + pub(crate) fn as_debug_https(&self) -> &DebugHTTPSServer { debug_assert!(matches!(self.tag, AnyServerTag::DebugHTTPSServer)); // SAFETY: tag-matched non-null `NewServer`; see `as_http`. unsafe { &*self.ptr.cast::() } @@ -3706,6 +3905,12 @@ impl AnyServer { /// Read-only accessors MUST use this form so holding the returned reference /// while calling another dispatch method does not materialize an aliasing /// `&mut NewServer` (Stacked-Borrows UB). +/// +/// Crate-visible (with [`any_server_dispatch_mut`]) so crate-internal +/// extensions of `AnyServer` — e.g. the dev-server accessors in +/// `bake/DevServer.rs` — can dispatch without this module naming their types. +/// Expansions reference `AnyServerTag` and the four server aliases, so those +/// must be in scope at the call site. macro_rules! any_server_dispatch { ($self:expr, |$s:ident| $body:expr) => {{ let this = $self; @@ -3729,6 +3934,7 @@ macro_rules! any_server_dispatch { } }}; } +pub(crate) use any_server_dispatch; /// Dispatch over the four `NewServer` monomorphizations (exclusive `&mut` /// borrow). Only for callers that mutate server state — never use this for @@ -3763,6 +3969,7 @@ macro_rules! any_server_dispatch_mut { } }}; } +pub(crate) use any_server_dispatch_mut; /// Dispatch over the four `NewServer` monomorphizations, simultaneously /// downcasting an [`uws::AnyResponse`] to the matching `*mut Response`. @@ -3908,8 +4115,8 @@ impl AnyServer { pub(crate) fn set_inspector_server_id(&mut self, id: jsc::DebuggerId) { any_server_dispatch_mut!(self, |s| { s.inspector_server_id = id; - if let Some(dev_server) = s.dev_server.as_deref_mut() { - dev_server.inspector_server_id = id; + if let Some(dev_server) = s.dev_server.as_mut() { + dev_server.set_inspector_server_id(id); } }) } @@ -3935,6 +4142,10 @@ impl AnyServer { any_server_dispatch_mut!(self, |s| s.on_static_request_complete()) } + // `AnyServer::dev_server`/`dev_server_mut` (the typed views over the + // `dev_server` slot) are defined next to the dev server itself in + // `crate::bake` — see the `impl AnyServer` block in `bake/DevServer.rs`. + pub(crate) fn stop(&mut self, abrupt: bool) { any_server_dispatch_mut!(self, |s| s.stop(abrupt)) } @@ -3983,55 +4194,47 @@ impl AnyServer { /// Wraps a stack-lifetime µWS request into a /// JS-visible `Request` + heap `RequestContext` so it can outlive the - /// handler frame (used by bake's deferred bundling path). + /// handler frame (used by the dev server's deferred bundling path). + /// `create_js_request` selects how the JS `Request` is materialized. pub(crate) fn prepare_and_save_js_request_context( &self, req: &mut uws::Request, resp: uws::AnyResponse, global: &jsc::JSGlobalObject, + create_js_request: CreateJsRequest, method: Option, ) -> jsc::JsResult> { let req: &mut uws_sys::Request = req; Ok(any_server_dispatch_resp!(self, resp, |s, r| { // `s` is the live `*mut NewServer` carried in `self.ptr`, // tagged at construction in `AnyServer::from`. - let Some(p) = NewServer::prepare_js_request_context( - s, - req, - r, - None, - CreateJsRequest::Bake, - method, - ) else { + let Some(p) = + NewServer::prepare_js_request_context(s, req, r, None, create_js_request, method) + else { return Ok(None); }; Some(p.save(global, req, r)) })) } - /// Invoke the user's route handler for a request that - /// was deferred (bake bundle-then-serve flow). + /// Invoke the user's route handler for a request that was deferred (the + /// dev server's bundle-then-serve flow). See [`NewServer::on_saved_request`] + /// for the `create_js_request` contract. pub(crate) fn on_saved_request( &self, req: SavedRequestUnion<'_>, resp: uws::AnyResponse, callback: jsc::JSValue, extra_args: [jsc::JSValue; EXTRA_ARG_COUNT], + create_js_request: CreateJsRequest, ) { // `s` is the live `*mut NewServer` carried in `self.ptr`, // tagged at construction in `AnyServer::from`. any_server_dispatch_resp!(self, resp, |s, r| { - NewServer::on_saved_request(s, req, r, callback, extra_args) + NewServer::on_saved_request(s, req, r, callback, extra_args, create_js_request) }) } - /// Mutable handle to the DevServer (when configured). HTMLBundle's request - /// path mutates DevServer state (`respond_for_html_bundle`). - #[allow(clippy::mut_from_ref)] // dispatched through the tagged raw `self.ptr` - pub(crate) fn dev_server_mut(&self) -> Option<&mut crate::bake::DevServer::DevServer> { - any_server_dispatch_mut!(self, |s| s.dev_server.as_deref_mut()) - } - /// Returns: /// - `Ready(None)` if no plugin has to be loaded /// - `Err` if there is a cached failure. Currently, this requires restarting the entire server. diff --git a/src/runtime/server/server_body.rs b/src/runtime/server/server_body.rs index 7d88a0729c54..601ddf563abf 100644 --- a/src/runtime/server/server_body.rs +++ b/src/runtime/server/server_body.rs @@ -5,9 +5,6 @@ use std::io::Write as _; use crate::api::js_bundler::PluginJscExt as _; use crate::api::{SocketAddress, js_bundler as JSBundler}; -use crate::bake::dev_server::DevServer; -use crate::bake::framework_router as FrameworkRouter; -use crate::bake::{self as bake}; use crate::node::types::PathLikeExt as _; use crate::webcore::BlobExt; use crate::webcore::body::Value as BodyValue; @@ -809,57 +806,13 @@ impl AnyRoute { )))); } - let style: FrameworkRouter::Style = - FrameworkRouter::Style::from_js(style_js.unwrap(), global)?; - // 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"/") - } else { - Cow::Owned(path[..path.len() - 2].to_vec()) - }; - init_ctx - .framework_router_list - .push(bake::FileSystemRouterType { - root: Cow::Owned(relative_root.to_vec()), - 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"), - ignore_underscores: true, - ignore_dirs: vec![ - Cow::Borrowed(b"node_modules".as_slice()), - Cow::Borrowed(b".git".as_slice()), - ], - extensions: vec![ - Cow::Borrowed(b".tsx".as_slice()), - Cow::Borrowed(b".jsx".as_slice()), - ], - allow_layouts: true, - }); - - // `@typeInfo(FrameworkRouter.Type.Index).@"enum".tag_type` → the index newtype's backing-int MAX. - let limit = u8::MAX as usize; - if init_ctx.framework_router_list.len() > limit { - return Err(global.throw_invalid_arguments(format_args!( - "Too many framework routers. Maximum is {}.", - limit - ))); - } - return Ok(Some(AnyRoute::FrameworkRouter( - FrameworkRouter::TypeIndex::init( - u8::try_from(init_ctx.framework_router_list.len() - 1).expect("int cast"), - ), - ))); + let type_index = init_ctx.framework_router_from_js( + global, + path, + relative_root, + style_js.unwrap(), + )?; + return Ok(Some(AnyRoute::FrameworkRouter(type_index))); } } @@ -878,11 +831,33 @@ impl AnyRoute { } } +/// Compile-time seam: the dev-server module supplies the concrete types of +/// the framework-router collection state on [`ServerInitContext`] by +/// implementing this on [`FrameworkRouterSeam`] (in `FrameworkRouter.rs`). +/// `Mount` values are only stored and moved here: they are written by the +/// dev-server route parser (`ServerInitContext::framework_router_from_js`) +/// and consumed by the dev-server options derivation at listen time. +/// `StringAllocations` is also written by this file's `{ dir }` parsing +/// (`AnyRoute::from_js` tracks the directory string before deciding between +/// a `DirectoryRoute` and a framework-router mount), so a replacement must +/// keep that `track` entry point. +pub trait FrameworkRouterTypes { + /// One parsed `{ dir, style }` framework-router mount. + type Mount; + /// Owns the JS string refs backing the mounts' borrowed bytes. + type StringAllocations: Default; +} + +/// Type-level carrier for the dev-server module's [`FrameworkRouterTypes`] +/// impl (uninhabited; never instantiated). +pub enum FrameworkRouterSeam {} + pub struct ServerInitContext<'a> { pub(crate) dedupe_html_bundle_map: HashMap<*const HTMLBundle, RefPtr>, - pub(crate) js_string_allocations: bake::StringRefList, + pub(crate) js_string_allocations: + ::StringAllocations, pub global: &'a JSGlobalObject, - pub(crate) framework_router_list: Vec, + pub(crate) framework_router_list: Vec<::Mount>, pub(crate) user_routes: &'a mut Vec, } @@ -896,6 +871,15 @@ pub struct ServePlugins { // Reference count is incremented while there are other objects waiting on plugin loads. // Maps to bun_ptr::IntrusiveRc — *ServePlugins crosses FFI as promise context ptr. +/// Notified when a pending [`ServePlugins`] load settles. The dev server's +/// implementation lives in `crate::bake`; the plugin loader stores the +/// consumer type-erased so it stays agnostic of the concrete type. +pub trait ServePluginsConsumer { + fn on_plugins_resolved(&mut self, plugins: Option<*mut JSBundler::Plugin>) + -> crate::Result<()>; + fn on_plugins_rejected(&mut self) -> crate::Result<()>; +} + pub enum ServePluginsState { Unqueued(Box<[Box<[u8]>]>), Pending { @@ -903,13 +887,14 @@ pub enum ServePluginsState { plugin: Box, promise: jsc::JSPromiseStrong, html_bundle_routes: Vec<*mut html_bundle::Route>, - // LIFETIMES.tsv classifies this BORROW_PARAM (`Option<&'a DevServer>`), - // but `ServePlugins` is a refcounted heap object handed across FFI as + // LIFETIMES.tsv classifies this BORROW_PARAM (`Option<&'a _>`), but + // `ServePlugins` is a refcounted heap object handed across FFI as // a raw promise-context pointer with dynamic lifetime, so a borrowed - // `&'a DevServer` cannot be expressed here. Back-reference invariant: - // the DevServer outlives the pending plugin load (see the SAFETY - // comments at the deref sites in `on_plugins_resolved`/`_rejected`). - dev_server: Option>, + // `&'a dyn ServePluginsConsumer` cannot be expressed here. + // Back-reference invariant: the consumer (the dev server) outlives the + // pending plugin load (see the SAFETY comments at the deref sites in + // `on_plugins_resolved`/`_rejected`). + consumer: Option>, }, Loaded(Box), /// Error information is not stored as it is already reported. @@ -931,7 +916,10 @@ pub enum ServePluginsCallback<'a> { /// (mutation goes through `Cell`/`JsCell`), so the `*mut` spelling is /// signature-only; callers pass `Route::as_ctx_ptr(&self)`. HtmlBundleRoute(*mut html_bundle::Route), - DevServer(&'a DevServer), + /// Type-erased consumer (the dev server); stored as a `NonNull` back-ref + /// in [`ServePluginsState::Pending`] while the load is in flight (hence + /// the explicit `'static` object bound — the borrow is only for this call). + Consumer(&'a (dyn ServePluginsConsumer + 'static)), } impl ServePlugins { @@ -994,7 +982,7 @@ impl ServePlugins { } ServePluginsState::Pending { html_bundle_routes, - dev_server, + consumer, .. } => { match cb { @@ -1006,13 +994,12 @@ impl ServePlugins { unsafe { bun_ptr::RefCount::::ref_(route) }; html_bundle_routes.push(route); } - ServePluginsCallback::DevServer(server) => { - debug_assert!( - dev_server.is_none() - || dev_server.map(|p| p.as_ptr().cast_const()) - == Some(std::ptr::from_ref(server)) - ); // one dev server per server - *dev_server = Some(NonNull::from(server)); + ServePluginsCallback::Consumer(new_consumer) => { + debug_assert!(consumer.is_none_or(|p| core::ptr::addr_eq( + p.as_ptr().cast_const(), + std::ptr::from_ref(new_consumer), + ))); // one consumer (the dev server) per server + *consumer = Some(NonNull::from(new_consumer)); } } return Ok(GetOrStartLoadResult::Pending); @@ -1061,7 +1048,7 @@ impl ServePlugins { promise: jsc::JSPromiseStrong::init(global), plugin, html_bundle_routes: Vec::new(), - dev_server: None, + consumer: None, }; global.bun_vm().event_loop_mut().enter(); @@ -1128,7 +1115,7 @@ impl ServePlugins { debug_assert!(matches!(self.state, ServePluginsState::Pending { .. })); let ServePluginsState::Pending { plugin, - dev_server, + consumer, html_bundle_routes, promise, } = mem::replace(&mut self.state, ServePluginsState::Err) @@ -1154,11 +1141,12 @@ impl ServePlugins { // SAFETY: paired with the `ref_` taken when the route was pushed. unsafe { bun_ptr::RefCount::::deref(route) }; } - if let Some(mut server) = dev_server { - // SAFETY: dev_server outlives plugin load (stored as a back-reference - // by `get_or_start_load`; the owning Box is held by the - // server instance, which itself holds a counted ref on `self`). - bun_core::handle_oom(unsafe { server.as_mut() }.on_plugins_resolved(Some( + if let Some(mut consumer) = consumer { + // SAFETY: the consumer outlives the plugin load (stored as a + // back-reference by `get_or_start_load`; its owning allocation is + // held by the server instance, which itself holds a counted ref on + // `self`). + bun_core::handle_oom(unsafe { consumer.as_mut() }.on_plugins_resolved(Some( std::ptr::from_ref::(plugin_ref).cast_mut(), ))); } @@ -1168,7 +1156,7 @@ impl ServePlugins { debug_assert!(matches!(self.state, ServePluginsState::Pending { .. })); let ServePluginsState::Pending { plugin, - dev_server, + consumer, html_bundle_routes, promise, } = mem::replace(&mut self.state, ServePluginsState::Err) @@ -1186,9 +1174,9 @@ impl ServePlugins { // SAFETY: route was ref'd when stored; pair with that ref unsafe { bun_ptr::RefCount::::deref(route) }; } - if let Some(mut server) = dev_server { - // SAFETY: dev_server outlives plugin load - bun_core::handle_oom(unsafe { server.as_mut() }.on_plugins_rejected()); + if let Some(mut consumer) = consumer { + // SAFETY: the consumer outlives the plugin load (see `handle_on_resolve`) + bun_core::handle_oom(unsafe { consumer.as_mut() }.on_plugins_rejected()); } Output::err_generic("Failed to load plugins for Bun.serve:", ()); @@ -2393,7 +2381,7 @@ where global, &mut args_slice, server_config::FromJSOptions { - allow_bake_config: false, + allow_dev_server_options: false, is_fetch_required: true, has_user_routes: !self.user_routes.is_empty(), }, @@ -2907,7 +2895,7 @@ where ) { jsc::mark_binding!(); if !matches!(self.config.address, server_config::Address::Unix(_)) - && (!bake::is_allowed_host_header(req, Some(&self.config.address)) + && (!crate::bake::is_allowed_host_header(req, Some(&self.config.address)) || !resp .get_remote_socket_info() .is_some_and(|address| address.is_loopback())) @@ -3337,11 +3325,13 @@ where Some(PreparedRequestFor { js_request: match create_js_request { CreateJsRequest::Yes => request_object.to_js(&server.global()), - CreateJsRequest::Bake => match request_object.to_js_for_bake(&server.global()) { - Ok(v) => v, - Err(JsError::OutOfMemory) => bun_core::out_of_memory(), - Err(_) => return None, - }, + CreateJsRequest::Custom(materialize) => { + match materialize(request_object, &server.global()) { + Ok(v) => v, + Err(JsError::OutOfMemory) => bun_core::out_of_memory(), + Err(_) => return None, + } + } CreateJsRequest::No => JSValue::ZERO, }, request_object: request_object_ptr, @@ -3580,7 +3570,7 @@ where } let authorized = 'brk: { - let Some(dev_server) = self.dev_server.as_deref() else { + let Some(dev_server) = self.dev_server.as_ref() else { break 'brk false; }; @@ -3588,7 +3578,7 @@ where // DNS-rebound origin connects from 127.0.0.1 but presents the // attacker's hostname in `Host`. Apply the same Host allowlist as // the `/_bun/*` routes before disclosing the project root path. - if !bake::is_allowed_dev_host(dev_server, req) { + if !dev_server.is_allowed_host(req) { break 'brk false; }