From 4e960d5f5ceacf6ed95da14153131c48a0326f36 Mon Sep 17 00:00:00 2001 From: claude-liminal <264858718+claude-liminal@users.noreply.github.com> Date: Mon, 23 Mar 2026 17:46:16 -0700 Subject: [PATCH 01/19] feat(build): add browser dep pre-bundling for unbundled dev serving Phase 1 of the unbundled client dev serving architecture. Adds: - client_dep_bundle.rs: Bundles React + npm deps as ESM for the browser - Import map generation for HTML injection - /_rex/dep/ route serving pre-bundled deps with immutable caching - AppState.client_deps (OnceLock) and HotState.import_map_json - Lazy loading during ensure_initialized(), not at startup Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/rex_build/src/client_dep_bundle.rs | 281 ++++++++++++++++++ crates/rex_build/src/lib.rs | 1 + crates/rex_dev/src/rebuild.rs | 2 + .../rex_server/src/handlers/test_support.rs | 2 + crates/rex_server/src/rex.rs | 2 + crates/rex_server/src/server.rs | 41 +++ crates/rex_server/src/state.rs | 61 +++- 7 files changed, 389 insertions(+), 1 deletion(-) create mode 100644 crates/rex_build/src/client_dep_bundle.rs diff --git a/crates/rex_build/src/client_dep_bundle.rs b/crates/rex_build/src/client_dep_bundle.rs new file mode 100644 index 0000000..e01d9dc --- /dev/null +++ b/crates/rex_build/src/client_dep_bundle.rs @@ -0,0 +1,281 @@ +//! Browser-side dependency pre-bundling for unbundled dev serving. +//! +//! Bundles React and npm deps as self-contained ESM modules for the browser. +//! These are served via `/_rex/dep/{specifier}.js` and mapped through an +//! HTML import map so user source files can use bare specifiers (`react`, etc.). +//! +//! Unlike `server_dep_bundle` (which targets V8 with react-server conditions), +//! this uses standard browser conditions and bundles `react-dom/client` for +//! hydration instead of `react-dom/server`. No V8 polyfills are injected. + +use crate::build_utils::runtime_client_dir; +use crate::esm_transform::DepImport; +use anyhow::Result; +use rex_core::RexConfig; +use std::collections::HashMap; +use tracing::debug; + +/// Result of browser dep pre-bundling. +pub struct ClientDepBundle { + /// Mapping of URL key → ESM source code. + /// Keys use URL-safe encoding (e.g., "react__jsx-runtime" for "react/jsx-runtime"). + pub modules: HashMap, + /// The generated import map JSON string for injection into HTML `\n" + )); + } + // CSS: inline content to avoid render-blocking network requests for css in params.css_files { if let Some(content) = params.css_contents.get(css) { @@ -107,18 +117,32 @@ pub fn assemble_document(params: &DocumentParams<'_>) -> String { )); } + let unbundled = params.import_map_json.is_some(); + // _app client chunk (must load before page scripts for hydration wrapping) if let Some(app) = params.app_script { - html.push_str(&format!( - " \n" - )); + if unbundled { + html.push_str(&format!( + " \n" + )); + } else { + html.push_str(&format!( + " \n" + )); + } } - // Client chunks (ESM bundles produced by rolldown) + // Client chunks (ESM bundles produced by rolldown, or /_rex/entry/ URLs in dev) for script in params.client_scripts { - html.push_str(&format!( - " \n" - )); + if unbundled { + html.push_str(&format!( + " \n" + )); + } else { + html.push_str(&format!( + " \n" + )); + } } // Client-side router (must load after page scripts register __REX_RENDER__) @@ -142,7 +166,7 @@ pub fn assemble_document(params: &DocumentParams<'_>) -> String { /// `` (or ``, `