From 0b0a4adc085f11cd43411d93d934981af6ec7643 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 6 Oct 2025 17:10:56 -0700 Subject: [PATCH 1/3] allow `--server` only builds! --- examples/07-fullstack/ssr-only/src/main.rs | 2 +- packages/cli/src/build/builder.rs | 17 ++++-- packages/cli/src/build/context.rs | 11 +++- packages/cli/src/build/pre_render.rs | 2 +- packages/cli/src/build/request.rs | 18 +++--- packages/cli/src/cli/build.rs | 12 ++-- packages/cli/src/cli/bundle.rs | 11 ++-- packages/cli/src/cli/hotpatch.rs | 16 ++++- packages/cli/src/serve/mod.rs | 4 +- packages/cli/src/serve/output.rs | 4 +- packages/cli/src/serve/runner.rs | 68 ++++++++++++---------- packages/cli/src/serve/server.rs | 2 +- 12 files changed, 102 insertions(+), 65 deletions(-) diff --git a/examples/07-fullstack/ssr-only/src/main.rs b/examples/07-fullstack/ssr-only/src/main.rs index b041006120..0f58cd2672 100644 --- a/examples/07-fullstack/ssr-only/src/main.rs +++ b/examples/07-fullstack/ssr-only/src/main.rs @@ -53,7 +53,7 @@ fn Post(id: u32) -> Element { async fn get_post(id: u32) -> Result { match id { 1 => Ok("first post".to_string()), - 2 => Ok("second post".to_string()), + 2 => Ok("second post - surreal!!!!".to_string()), _ => HttpError::not_found("Post not found")?, } } diff --git a/packages/cli/src/build/builder.rs b/packages/cli/src/build/builder.rs index 33c4fa83cc..efaf87d5c8 100644 --- a/packages/cli/src/build/builder.rs +++ b/packages/cli/src/build/builder.rs @@ -154,19 +154,20 @@ impl AppBuilder { } /// Create a new `AppBuilder` and immediately start a build process. - pub fn started(request: &BuildRequest, mode: BuildMode) -> Result { + pub fn started(request: &BuildRequest, mode: BuildMode, build_id: BuildId) -> Result { let mut builder = Self::new(request)?; - builder.start(mode); + builder.start(mode, build_id); Ok(builder) } - pub(crate) fn start(&mut self, mode: BuildMode) { + pub(crate) fn start(&mut self, mode: BuildMode, build_id: BuildId) { self.build_task = tokio::spawn({ let request = self.build.clone(); let tx = self.tx.clone(); async move { let ctx = BuildContext { mode, + build_id, tx: tx.clone(), }; request.verify_tooling(&ctx).await?; @@ -290,10 +291,12 @@ impl AppBuilder { update } - pub(crate) fn patch_rebuild(&mut self, changed_files: Vec) { + pub(crate) fn patch_rebuild(&mut self, changed_files: Vec, build_id: BuildId) { // We need the rustc args from the original build to pass to the new build let Some(artifacts) = self.artifacts.as_ref().cloned() else { - tracing::warn!("Ignoring patch rebuild since there is no existing build."); + tracing::warn!( + "Ignoring patch rebuild for {build_id:?} since there is no existing build." + ); return; }; @@ -327,6 +330,7 @@ impl AppBuilder { self.build_task = tokio::spawn({ let request = self.build.clone(); let ctx = BuildContext { + build_id, tx: self.tx.clone(), mode: BuildMode::Thin { changed_files, @@ -340,7 +344,7 @@ impl AppBuilder { } /// Restart this builder with new build arguments. - pub(crate) fn start_rebuild(&mut self, mode: BuildMode) { + pub(crate) fn start_rebuild(&mut self, mode: BuildMode, build_id: BuildId) { // Abort all the ongoing builds, cleaning up any loose artifacts and waiting to cleanly exit // And then start a new build, resetting our progress/stage to the beginning and replacing the old tokio task self.abort_all(BuildStage::Restarting); @@ -351,6 +355,7 @@ impl AppBuilder { let ctx = BuildContext { tx: self.tx.clone(), mode, + build_id, }; async move { request.build(&ctx).await } }); diff --git a/packages/cli/src/build/context.rs b/packages/cli/src/build/context.rs index 3cf7ac8732..3810c9ca38 100644 --- a/packages/cli/src/build/context.rs +++ b/packages/cli/src/build/context.rs @@ -16,6 +16,7 @@ use std::{path::PathBuf, process::ExitStatus}; pub struct BuildContext { pub tx: ProgressTx, pub mode: BuildMode, + pub build_id: BuildId, } pub type ProgressTx = UnboundedSender; @@ -24,8 +25,8 @@ pub type ProgressRx = UnboundedReceiver; #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] pub struct BuildId(pub(crate) usize); impl BuildId { - pub const CLIENT: Self = Self(0); - pub const SERVER: Self = Self(1); + pub const PRIMARY: Self = Self(0); + pub const SECONDARY: Self = Self(1); } #[allow(clippy::large_enum_variant)] @@ -78,6 +79,12 @@ pub enum BuilderUpdate { } impl BuildContext { + /// Returns true if this is a client build - basically, is this the primary build? + /// We try not to duplicate work between client and server builds, like asset copying. + pub(crate) fn is_primary_build(&self) -> bool { + self.build_id == BuildId::PRIMARY + } + pub(crate) fn status_wasm_bindgen_start(&self) { _ = self.tx.unbounded_send(BuilderUpdate::Progress { stage: BuildStage::RunningBindgen, diff --git a/packages/cli/src/build/pre_render.rs b/packages/cli/src/build/pre_render.rs index 5b7010d350..c7c87ad00b 100644 --- a/packages/cli/src/build/pre_render.rs +++ b/packages/cli/src/build/pre_render.rs @@ -45,7 +45,7 @@ pub(crate) async fn pre_render_static_routes( devserver_ip, Some(fullstack_address), false, - BuildId::SERVER, + BuildId::SECONDARY, ); // Run the server executable let _child = Command::new(&server_exe) diff --git a/packages/cli/src/build/request.rs b/packages/cli/src/build/request.rs index da0bca9818..2fe0c2981a 100644 --- a/packages/cli/src/build/request.rs +++ b/packages/cli/src/build/request.rs @@ -320,9 +320,9 @@ //! - xbuild: use crate::{ - AndroidTools, BuildContext, BundleFormat, DioxusConfig, Error, LinkAction, LinkerFlavor, - Platform, Renderer, Result, RustcArgs, TargetArgs, TraceSrc, WasmBindgen, WasmOptConfig, - Workspace, DX_RUSTC_WRAPPER_ENV_VAR, + AndroidTools, BuildContext, BuildId, BundleFormat, DioxusConfig, Error, LinkAction, + LinkerFlavor, Platform, Renderer, Result, RustcArgs, TargetArgs, TraceSrc, WasmBindgen, + WasmOptConfig, Workspace, DX_RUSTC_WRAPPER_ENV_VAR, }; use anyhow::{bail, Context}; use cargo_metadata::diagnostic::Diagnostic; @@ -454,6 +454,7 @@ pub struct BuildArtifacts { pub(crate) mode: BuildMode, pub(crate) patch_cache: Option>, pub(crate) depinfo: RustcDepInfo, + pub(crate) build_id: BuildId, } impl BuildRequest { @@ -989,10 +990,10 @@ impl BuildRequest { _ = std::fs::File::create_new(self.windows_command_file()); if !matches!(ctx.mode, BuildMode::Thin { .. }) { - self.prepare_build_dir()?; + self.prepare_build_dir(ctx)?; } - if self.bundle == BundleFormat::Server { + if !ctx.is_primary_build() { return Ok(()); } @@ -1270,6 +1271,7 @@ impl BuildRequest { depinfo, root_dir: self.root_dir(), patch_cache: None, + build_id: ctx.build_id, }) } @@ -1460,7 +1462,7 @@ impl BuildRequest { /// Should be the same on all platforms - just copy over the assets from the manifest into the output directory async fn write_assets(&self, ctx: &BuildContext, assets: &AssetManifest) -> Result<()> { // Server doesn't need assets - web will provide them - if self.bundle == BundleFormat::Server { + if !ctx.is_primary_build() { return Ok(()); } @@ -4320,14 +4322,14 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{ /// - extra scaffolding /// /// It's not guaranteed that they're different from any other folder - pub(crate) fn prepare_build_dir(&self) -> Result<()> { + pub(crate) fn prepare_build_dir(&self, ctx: &BuildContext) -> Result<()> { use std::fs::{create_dir_all, remove_dir_all}; use std::sync::OnceLock; static INITIALIZED: OnceLock> = OnceLock::new(); let success = INITIALIZED.get_or_init(|| { - if self.bundle != BundleFormat::Server { + if ctx.is_primary_build() { _ = remove_dir_all(self.exe_dir()); } diff --git a/packages/cli/src/cli/build.rs b/packages/cli/src/cli/build.rs index 19cc87d355..68789ed847 100644 --- a/packages/cli/src/cli/build.rs +++ b/packages/cli/src/cli/build.rs @@ -1,8 +1,8 @@ use dioxus_dx_wire_format::StructuredBuildArtifacts; use crate::{ - cli::*, Anonymized, AppBuilder, BuildArtifacts, BuildMode, BuildRequest, BundleFormat, - TargetArgs, Workspace, + cli::*, Anonymized, AppBuilder, BuildArtifacts, BuildId, BuildMode, BuildRequest, BundleFormat, + Platform, TargetArgs, Workspace, }; /// Build the Rust Dioxus app and all of its assets. @@ -128,6 +128,10 @@ impl CommandWithPlatformOverrides { BuildRequest::new(&server_args.build_arguments, workspace.clone()).await?, ); } + None if client_args.platform == Platform::Server => { + // If the user requests a server build with `--server`, then we don't need to build a separate server binary. + // There's no client to use, so even though fullstack is true, we only build the server. + } None => { let mut args = self.shared.build_arguments.clone(); args.platform = crate::Platform::Server; @@ -171,7 +175,7 @@ impl CommandWithPlatformOverrides { request: &BuildRequest, mode: BuildMode, ) -> Result { - AppBuilder::started(request, mode)? + AppBuilder::started(request, mode, BuildId::PRIMARY)? .finish_build() .await .inspect(|_| { @@ -189,7 +193,7 @@ impl CommandWithPlatformOverrides { }; // If the server is present, we need to build it as well - let mut server_build = AppBuilder::started(server, mode)?; + let mut server_build = AppBuilder::started(server, mode, BuildId::SECONDARY)?; let server_artifacts = server_build.finish_build().await?; // Run SSG and cache static routes diff --git a/packages/cli/src/cli/bundle.rs b/packages/cli/src/cli/bundle.rs index 313c0199ca..ba6f0e9e3b 100644 --- a/packages/cli/src/cli/bundle.rs +++ b/packages/cli/src/cli/bundle.rs @@ -1,4 +1,4 @@ -use crate::{AppBuilder, BuildArgs, BuildMode, BuildRequest, BundleFormat}; +use crate::{AppBuilder, BuildArgs, BuildId, BuildMode, BuildRequest, BundleFormat}; use anyhow::{bail, Context}; use path_absolutize::Absolutize; use std::collections::HashMap; @@ -40,16 +40,17 @@ impl Bundle { let BuildTargets { client, server } = self.args.into_targets().await?; let mut server_artifacts = None; - let client_artifacts = AppBuilder::started(&client, BuildMode::Base { run: false })? - .finish_build() - .await?; + let client_artifacts = + AppBuilder::started(&client, BuildMode::Base { run: false }, BuildId::PRIMARY)? + .finish_build() + .await?; tracing::info!(path = ?client.root_dir(), "Client build completed successfully! 🚀"); if let Some(server) = server.as_ref() { // If the server is present, we need to build it as well server_artifacts = Some( - AppBuilder::started(server, BuildMode::Base { run: false })? + AppBuilder::started(server, BuildMode::Base { run: false }, BuildId::SECONDARY)? .finish_build() .await?, ); diff --git a/packages/cli/src/cli/hotpatch.rs b/packages/cli/src/cli/hotpatch.rs index a2d619afdd..77f213c184 100644 --- a/packages/cli/src/cli/hotpatch.rs +++ b/packages/cli/src/cli/hotpatch.rs @@ -1,5 +1,5 @@ use crate::{ - platform_override::CommandWithPlatformOverrides, AppBuilder, BuildArgs, BuildMode, + platform_override::CommandWithPlatformOverrides, AppBuilder, BuildArgs, BuildId, BuildMode, HotpatchModuleCache, Result, StructuredOutput, }; use anyhow::Context; @@ -34,8 +34,16 @@ impl HotpatchTip { pub async fn run(self) -> Result { let targets = self.build_args.into_targets().await?; + let patch_server = self.patch_server.unwrap_or(false); + + let build_id = if patch_server { + BuildId::SECONDARY + } else { + BuildId::PRIMARY + }; + // Select which target to patch - let request = if self.patch_server.unwrap_or_default() { + let request = if patch_server { targets.server.as_ref().context("No server to patch!")? } else { &targets.client @@ -68,7 +76,9 @@ impl HotpatchTip { cache: cache.clone(), }; - let artifacts = AppBuilder::started(request, mode)?.finish_build().await?; + let artifacts = AppBuilder::started(request, mode, build_id)? + .finish_build() + .await?; let patch_exe = request.patch_exe(artifacts.time_start); Ok(StructuredOutput::Hotpatch { diff --git a/packages/cli/src/serve/mod.rs b/packages/cli/src/serve/mod.rs index 7e54e8abfd..176e192cdd 100644 --- a/packages/cli/src/serve/mod.rs +++ b/packages/cli/src/serve/mod.rs @@ -109,12 +109,12 @@ pub(crate) async fn serve_all(args: ServeArgs, tracer: &TraceController) -> Resu pid, } => { devserver - .send_hotreload(builder.applied_hot_reload_changes(BuildId::CLIENT)) + .send_hotreload(builder.applied_hot_reload_changes(BuildId::PRIMARY)) .await; if builder.server.is_some() { devserver - .send_hotreload(builder.applied_hot_reload_changes(BuildId::SERVER)) + .send_hotreload(builder.applied_hot_reload_changes(BuildId::SECONDARY)) .await; } diff --git a/packages/cli/src/serve/output.rs b/packages/cli/src/serve/output.rs index e0404315bd..e2bd677aa9 100644 --- a/packages/cli/src/serve/output.rs +++ b/packages/cli/src/serve/output.rs @@ -268,12 +268,12 @@ impl Output { } KeyCode::Char('D') => { return Ok(Some(ServeUpdate::OpenDebugger { - id: BuildId::SERVER, + id: BuildId::SECONDARY, })); } KeyCode::Char('d') => { return Ok(Some(ServeUpdate::OpenDebugger { - id: BuildId::CLIENT, + id: BuildId::PRIMARY, })); } KeyCode::Char('c') => { diff --git a/packages/cli/src/serve/runner.rs b/packages/cli/src/serve/runner.rs index 211e5b0b15..e34042b969 100644 --- a/packages/cli/src/serve/runner.rs +++ b/packages/cli/src/serve/runner.rs @@ -234,9 +234,9 @@ impl AppServer { false => BuildMode::Base { run: true }, }; - self.client.start(build_mode.clone()); + self.client.start(build_mode.clone(), BuildId::PRIMARY); if let Some(server) = self.server.as_mut() { - server.start(build_mode); + server.start(build_mode, BuildId::SECONDARY); } } @@ -273,14 +273,14 @@ impl AppServer { // Wait for the client to finish client_update = client_wait => { ServeUpdate::BuilderUpdate { - id: BuildId::CLIENT, + id: BuildId::PRIMARY, update: client_update, } } Some(server_update) = server_wait => { ServeUpdate::BuilderUpdate { - id: BuildId::SERVER, + id: BuildId::SECONDARY, update: server_update, } } @@ -491,17 +491,18 @@ impl AppServer { // A full rebuild is required when the user modifies static initializers which we haven't wired up yet. if needs_full_rebuild && self.automatic_rebuilds { if self.use_hotpatch_engine { - self.client.patch_rebuild(files.to_vec()); + self.client.patch_rebuild(files.to_vec(), BuildId::PRIMARY); if let Some(server) = self.server.as_mut() { - server.patch_rebuild(files.to_vec()); + server.patch_rebuild(files.to_vec(), BuildId::SECONDARY); } self.clear_hot_reload_changes(); self.clear_cached_rsx(); server.send_patch_start().await; } else { - self.client.start_rebuild(BuildMode::Base { run: true }); + self.client + .start_rebuild(BuildMode::Base { run: true }, BuildId::PRIMARY); if let Some(server) = self.server.as_mut() { - server.start_rebuild(BuildMode::Base { run: true }); + server.start_rebuild(BuildMode::Base { run: true }, BuildId::SECONDARY); } self.clear_hot_reload_changes(); self.clear_cached_rsx(); @@ -560,13 +561,14 @@ impl AppServer { devserver: &mut WebServer, ) -> Result<()> { // Make sure to save artifacts regardless of if we're opening the app or not - match artifacts.bundle { - BundleFormat::Server => { + match artifacts.build_id { + BuildId::PRIMARY => self.client.artifacts = Some(artifacts.clone()), + BuildId::SECONDARY => { if let Some(server) = self.server.as_mut() { server.artifacts = Some(artifacts.clone()); } } - _ => self.client.artifacts = Some(artifacts.clone()), + _ => {} } let should_open = self.client.stage == BuildStage::Success @@ -634,7 +636,7 @@ impl AppServer { fullstack_address, false, false, - BuildId::SERVER, + BuildId::SECONDARY, &self.server_args, ) .await?; @@ -649,7 +651,7 @@ impl AppServer { fullstack_address, open_browser, self.always_on_top, - BuildId::CLIENT, + BuildId::PRIMARY, &self.client_args, ) .await?; @@ -696,9 +698,10 @@ impl AppServer { false => BuildMode::Base { run: true }, }; - self.client.start_rebuild(build_mode.clone()); + self.client + .start_rebuild(build_mode.clone(), BuildId::PRIMARY); if let Some(s) = self.server.as_mut() { - s.start_rebuild(build_mode) + s.start_rebuild(build_mode, BuildId::SECONDARY); } self.clear_hot_reload_changes(); @@ -719,8 +722,8 @@ impl AppServer { .unwrap_or_default(); let jump_table = match id { - BuildId::CLIENT => self.client.hotpatch(bundle, cache).await, - BuildId::SERVER => { + BuildId::PRIMARY => self.client.hotpatch(bundle, cache).await, + BuildId::SECONDARY => { self.server .as_mut() .context("Server not found")? @@ -730,7 +733,7 @@ impl AppServer { _ => bail!("Invalid build id"), }?; - if id == BuildId::CLIENT { + if id == BuildId::PRIMARY { self.applied_client_hot_reload_message.jump_table = self.client.patches.last().cloned(); } @@ -759,11 +762,16 @@ impl AppServer { .context("Missing server jump table")?; devserver - .send_patch(server_jump_table, elapsed, BuildId::SERVER, server.pid) + .send_patch(server_jump_table, elapsed, BuildId::SECONDARY, server.pid) .await; devserver - .send_patch(client_jump_table, elapsed, BuildId::CLIENT, self.client.pid) + .send_patch( + client_jump_table, + elapsed, + BuildId::PRIMARY, + self.client.pid, + ) .await; } @@ -772,8 +780,8 @@ impl AppServer { pub(crate) fn get_build(&self, id: BuildId) -> Option<&AppBuilder> { match id { - BuildId::CLIENT => Some(&self.client), - BuildId::SERVER => self.server.as_ref(), + BuildId::PRIMARY => Some(&self.client), + BuildId::SECONDARY => self.server.as_ref(), _ => None, } } @@ -791,18 +799,18 @@ impl AppServer { pub(crate) fn applied_hot_reload_changes(&mut self, build: BuildId) -> HotReloadMsg { let mut msg = self.applied_client_hot_reload_message.clone(); - if build == BuildId::CLIENT { + if build == BuildId::PRIMARY { msg.jump_table = self.client.patches.last().cloned(); - msg.for_build_id = Some(BuildId::CLIENT.0 as _); + msg.for_build_id = Some(BuildId::PRIMARY.0 as _); if let Some(lib) = msg.jump_table.as_mut() { lib.lib = PathBuf::from("/").join(lib.lib.clone()); } } - if build == BuildId::SERVER { + if build == BuildId::SECONDARY { if let Some(server) = self.server.as_mut() { msg.jump_table = server.patches.last().cloned(); - msg.for_build_id = Some(BuildId::SERVER.0 as _); + msg.for_build_id = Some(BuildId::SECONDARY.0 as _); } } @@ -828,7 +836,7 @@ impl AppServer { pid: Option, ) { match build_id { - BuildId::CLIENT => { + BuildId::PRIMARY => { // multiple tabs on web can cause this to be called incorrectly, and it doesn't // make any sense anyways if self.client.build.bundle != BundleFormat::Web { @@ -840,7 +848,7 @@ impl AppServer { } } } - BuildId::SERVER => { + BuildId::SECONDARY => { if let Some(server) = self.server.as_mut() { server.aslr_reference = aslr_reference; } @@ -1146,10 +1154,10 @@ impl AppServer { } match build { - BuildId::CLIENT => { + BuildId::PRIMARY => { _ = self.client.open_debugger(dev).await; } - BuildId::SERVER => { + BuildId::SECONDARY => { if let Some(server) = self.server.as_mut() { _ = server.open_debugger(dev).await; } diff --git a/packages/cli/src/serve/server.rs b/packages/cli/src/serve/server.rs index 4886fd4f2c..d5fa49bd3d 100644 --- a/packages/cli/src/serve/server.rs +++ b/packages/cli/src/serve/server.rs @@ -148,7 +148,7 @@ impl WebServer { if let Some(new_socket) = new_hot_reload_socket { let aslr_reference = new_socket.aslr_reference; let pid = new_socket.pid; - let id = new_socket.build_id.unwrap_or(BuildId::CLIENT); + let id = new_socket.build_id.unwrap_or(BuildId::PRIMARY); drop(new_message); self.hot_reload_sockets.push(new_socket); From e4189236068d679b681e181a38e872e59f943743 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 6 Oct 2025 17:14:57 -0700 Subject: [PATCH 2/3] rollback example --- examples/07-fullstack/ssr-only/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/07-fullstack/ssr-only/src/main.rs b/examples/07-fullstack/ssr-only/src/main.rs index 0f58cd2672..b041006120 100644 --- a/examples/07-fullstack/ssr-only/src/main.rs +++ b/examples/07-fullstack/ssr-only/src/main.rs @@ -53,7 +53,7 @@ fn Post(id: u32) -> Element { async fn get_post(id: u32) -> Result { match id { 1 => Ok("first post".to_string()), - 2 => Ok("second post - surreal!!!!".to_string()), + 2 => Ok("second post".to_string()), _ => HttpError::not_found("Post not found")?, } } From 5ce9a7dc232c76644a06d1526a479ac6f31cbe31 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 7 Oct 2025 14:47:36 -0700 Subject: [PATCH 3/3] wip: much harder than I thought --- Cargo.lock | 155 +----------------- Cargo.toml | 7 + .../namespaced_server_functions.rs | 104 ++++++++++++ examples/07-fullstack/ssr-only/src/main.rs | 2 + packages/cli/Cargo.toml | 1 - packages/fullstack-core/src/lib.rs | 8 + packages/fullstack-macro/src/lib.rs | 121 +++++++++----- packages/fullstack-server/Cargo.toml | 2 +- packages/fullstack-server/src/serverfn.rs | 12 +- packages/fullstack/src/endpoint.rs | 24 +++ packages/fullstack/src/lib.rs | 3 + 11 files changed, 241 insertions(+), 198 deletions(-) create mode 100644 examples/07-fullstack/namespaced_server_functions.rs create mode 100644 packages/fullstack/src/endpoint.rs diff --git a/Cargo.lock b/Cargo.lock index d5441f6257..a9ce5c7d4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2330,7 +2330,7 @@ dependencies = [ "bevy_window", "bitflags 2.9.1", "bytemuck", - "codespan-reporting 0.11.1", + "codespan-reporting", "derive_more 1.0.0", "downcast-rs 2.0.1", "encase", @@ -3809,17 +3809,6 @@ dependencies = [ "unicode-width 0.1.14", ] -[[package]] -name = "codespan-reporting" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" -dependencies = [ - "serde", - "termcolor", - "unicode-width 0.2.0", -] - [[package]] name = "color" version = "0.3.1" @@ -4718,68 +4707,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "cxx" -version = "1.0.160" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1149bab7a5580cb267215751389597c021bfad13c0bb00c54e19559333764c" -dependencies = [ - "cc", - "cxxbridge-cmd", - "cxxbridge-flags", - "cxxbridge-macro", - "foldhash", - "link-cplusplus", -] - -[[package]] -name = "cxx-build" -version = "1.0.160" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aeeaf1aefae8e0f5141920a7ecbc64a22ab038d4b4ac59f2d19e0effafd5b53" -dependencies = [ - "cc", - "codespan-reporting 0.12.0", - "indexmap 2.10.0", - "proc-macro2", - "quote", - "scratch", - "syn 2.0.104", -] - -[[package]] -name = "cxxbridge-cmd" -version = "1.0.160" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c36ac1f9a72064b1f41fd7b49a4c1b3bf33b9ccb1274874dda6d264f57c55964" -dependencies = [ - "clap", - "codespan-reporting 0.12.0", - "indexmap 2.10.0", - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "cxxbridge-flags" -version = "1.0.160" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "170c6ff5d009663866857a91ebee55b98ea4d4b34e7d7aba6dc4a4c95cc7b748" - -[[package]] -name = "cxxbridge-macro" -version = "1.0.160" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4984a142211026786011a7e79fa22faa1eca1e9cbf0e60bffecfd57fd3db88f1" -dependencies = [ - "indexmap 2.10.0", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.104", -] - [[package]] name = "darling" version = "0.20.11" @@ -5294,7 +5221,6 @@ dependencies = [ "walrus", "wasm-bindgen-externref-xform", "wasm-encoder 0.235.0", - "wasm-opt", "wasm-split-cli", "wasmparser 0.235.0", "which 8.0.0", @@ -5567,6 +5493,7 @@ dependencies = [ "base64 0.22.1", "bytes", "ciborium", + "dashmap 6.1.0", "dioxus", "dioxus-html", "dioxus-ssr", @@ -10397,15 +10324,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "link-cplusplus" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a6f6da007f968f9def0d65a05b187e2960183de70c160204ecfccf0ee330212" -dependencies = [ - "cc", -] - [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -11027,7 +10945,7 @@ dependencies = [ "bit-set 0.8.0", "bitflags 2.9.1", "cfg_aliases", - "codespan-reporting 0.11.1", + "codespan-reporting", "hexf-parse", "indexmap 2.10.0", "log", @@ -11047,7 +10965,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2464f7395decfd16bb4c33fb0cb3b2c645cc60d051bc7fb652d3720bfb20f18" dependencies = [ "bit-set 0.5.3", - "codespan-reporting 0.11.1", + "codespan-reporting", "data-encoding", "indexmap 2.10.0", "naga", @@ -14552,12 +14470,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "scratch" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f6280af86e5f559536da57a45ebc84948833b3bee313a7dd25232e09c878a52" - [[package]] name = "scroll" version = "0.11.0" @@ -15783,12 +15695,6 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" -[[package]] -name = "strum" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" - [[package]] name = "strum" version = "0.26.3" @@ -15807,19 +15713,6 @@ dependencies = [ "strum_macros 0.27.1", ] -[[package]] -name = "strum_macros" -version = "0.24.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" -dependencies = [ - "heck 0.4.1", - "proc-macro2", - "quote", - "rustversion", - "syn 1.0.109", -] - [[package]] name = "strum_macros" version = "0.26.4" @@ -18520,46 +18413,6 @@ dependencies = [ "wasmparser 0.235.0", ] -[[package]] -name = "wasm-opt" -version = "0.116.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd87a4c135535ffed86123b6fb0f0a5a0bc89e50416c942c5f0662c645f679c" -dependencies = [ - "anyhow", - "libc", - "strum 0.24.1", - "strum_macros 0.24.3", - "tempfile", - "thiserror 1.0.69", - "wasm-opt-cxx-sys", - "wasm-opt-sys", -] - -[[package]] -name = "wasm-opt-cxx-sys" -version = "0.116.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c57b28207aa724318fcec6575fe74803c23f6f266fce10cbc9f3f116762f12e" -dependencies = [ - "anyhow", - "cxx", - "cxx-build", - "wasm-opt-sys", -] - -[[package]] -name = "wasm-opt-sys" -version = "0.116.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a1cce564dc768dacbdb718fc29df2dba80bd21cb47d8f77ae7e3d95ceb98cbe" -dependencies = [ - "anyhow", - "cc", - "cxx", - "cxx-build", -] - [[package]] name = "wasm-split-cli" version = "0.7.0-rc.0" diff --git a/Cargo.toml b/Cargo.toml index 040ac96394..b897c320da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -340,6 +340,7 @@ pin-project = { version = "1.1.10" } postcard = { version = "1.1.3", default-features = false } serde_urlencoded = "0.7" form_urlencoded = "1.2.1" +dashmap = "6.1.0" # desktop wry = { version = "0.52.1", default-features = false } @@ -466,6 +467,7 @@ bytes = { workspace = true } futures = { workspace = true } axum-core = { workspace = true } uuid = { workspace = true, features = ["v4", "serde"] } +dashmap = { workspace = true } [target.'cfg(target_arch = "wasm32")'.dev-dependencies] getrandom = { workspace = true, features = ["wasm_js"] } @@ -679,6 +681,11 @@ name = "flat_router" path = "examples/06-routing/flat_router.rs" doc-scrape-examples = true +[[example]] +name = "namespaced_server_functions" +path = "examples/07-fullstack/namespaced_server_functions.rs" +doc-scrape-examples = true + [[example]] name = "header_map" path = "examples/07-fullstack/header_map.rs" diff --git a/examples/07-fullstack/namespaced_server_functions.rs b/examples/07-fullstack/namespaced_server_functions.rs new file mode 100644 index 0000000000..27f54bdfc3 --- /dev/null +++ b/examples/07-fullstack/namespaced_server_functions.rs @@ -0,0 +1,104 @@ +//! This example demonstrates how to define namespaced server functions in Dioxus Fullstack. +//! +//! Namespaced Server Functions allow you to organize your server functions into logical groups, +//! making it possible to reuse groups of functions as a library across different projects. +//! +//! Namespaced server functions are defined as methods on a struct. The struct itself is the "state" +//! of this group of functions, and can hold any data you want to share across the functions. +//! +//! Unlike regular server functions, namespaced server functions are not automatically registered +//! with `dioxus::launch`. You must explicitly mount the server functions to a given route using the +//! `Endpoint::mount` function. From the client, you can then call the functions using regular method +//! call syntax. +//! +//! Namespaces are designed to make server functions easier to modularize and reuse, making it possible +//! to create a publishable library of server functions that other developers can easily integrate into +//! their own Dioxus Fullstack applications. + +use dioxus::fullstack::Endpoint; +use dioxus::prelude::*; + +fn main() { + #[cfg(not(feature = "server"))] + dioxus::launch(app); + + // On the server, we can customize the models and mount the server functions to a specific route. + // The `.endpoint()` extension method allows you to mount an `Endpoint` to an axum router. + #[cfg(feature = "server")] + dioxus::serve(|| async move { + // + todo!() + }); +} + +// We mount a namespace of server functions to the "/api/dogs" route. +// All calls to `DOGS` from the client will be sent to this route. +static DOGS: Endpoint = Endpoint::new("/api/dogs", || PetApi { pets: todo!() }); + +/// Our server functions will be associated with this struct. +struct PetApi { + /// we can add shared state here if we want + /// e.g. a database connection pool + /// + /// Since `PetApi` exists both on the client and server, we need to conditionally include + /// the database pool only on the server. + // #[cfg(feature = "server")] + pets: dashmap::DashMap, +} + +impl PetApi { + /// List all the pets in the database. + // #[get("/")] + async fn list(&self) -> Result> { + Ok(self.pets.iter().map(|entry| entry.key().clone()).collect()) + } + + /// Get the breed of a specific pet by name. + // #[get("/{name}")] + async fn get(&self, name: String) -> Result { + Ok(self + .pets + .get(&name) + .map(|entry| entry.value().clone()) + .or_not_found("pet not found")?) + } + + /// Add a new pet to the database. + // #[post("/{name}")] + async fn add(&self, name: String, breed: String) -> Result<()> { + self.pets.insert(name, breed); + Ok(()) + } + + /// Remove a pet from the database. + // #[delete("/{name}")] + async fn remove(&self, name: String) -> Result<()> { + self.pets.remove(&name).or_not_found("pet not found")?; + Ok(()) + } + + /// Update a pet's name in the database. + #[put("/{name}")] + async fn update(&self, name: String, breed: String) -> Result<()> { + self.pets.insert(breed.clone(), breed); + Ok(()) + } +} + +/// In our app, we can call the namespaced server functions using regular method call syntax, mixing +/// loaders, actions, and other hooks as normal. +fn app() -> Element { + let pets = use_loader(|| DOGS.list())?; + let add = use_action(|name, breed| DOGS.add(name, breed)); + let remove = use_action(|name| DOGS.remove(name)); + let update = use_action(|breed| DOGS.update(breed)); + + rsx! { + div { + h1 { "My Pets" } + ul { + + } + } + } +} diff --git a/examples/07-fullstack/ssr-only/src/main.rs b/examples/07-fullstack/ssr-only/src/main.rs index b041006120..d64344b007 100644 --- a/examples/07-fullstack/ssr-only/src/main.rs +++ b/examples/07-fullstack/ssr-only/src/main.rs @@ -8,6 +8,8 @@ //! //! To run this example, simply run `cargo run --package ssr-only` and navigate to `http://localhost:8080`. +use std::any::TypeId; + use dioxus::prelude::*; fn main() { diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index 34309d7e39..bd7154e0d2 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -106,7 +106,6 @@ tracing-subscriber = { version = "0.3.19", features = [ ] } console-subscriber = { version = "0.4.1", optional = true } tracing = { workspace = true } -wasm-opt = { version = "0.116.1", optional = true } ansi-to-tui = { workspace = true } ansi-to-html = { workspace = true } path-absolutize = { workspace = true } diff --git a/packages/fullstack-core/src/lib.rs b/packages/fullstack-core/src/lib.rs index e408a7bb3e..d5ea104a8b 100644 --- a/packages/fullstack-core/src/lib.rs +++ b/packages/fullstack-core/src/lib.rs @@ -11,6 +11,8 @@ mod server_future; mod streaming; mod transport; +use std::{any::Any, sync::Arc}; + pub use crate::errors::*; pub use crate::loader::*; pub use crate::server_cached::*; @@ -28,3 +30,9 @@ pub use httperror::*; #[derive(Clone, Default)] pub struct DioxusServerState {} + +impl DioxusServerState { + pub fn get_endpoint(&self) -> Option> { + todo!() + } +} diff --git a/packages/fullstack-macro/src/lib.rs b/packages/fullstack-macro/src/lib.rs index 855e8a039d..1745f265d5 100644 --- a/packages/fullstack-macro/src/lib.rs +++ b/packages/fullstack-macro/src/lib.rs @@ -205,8 +205,16 @@ fn route_impl_with_route( let function = syn::parse::(item)?; let server_args = route.server_args.clone(); + let mut function_on_server = function.clone(); function_on_server.sig.inputs.extend(server_args.clone()); + function_on_server.sig.ident = format_ident!("__server_fn_inner_{}", function.sig.ident); + let function_on_server_name = function_on_server.sig.ident.clone(); + + let mut self_token = function.sig.inputs.first().map(|arg| match arg { + FnArg::Receiver(receiver) => Some(receiver.self_token), + _ => None, + }); // Now we can compile the route let original_inputs = function @@ -214,7 +222,7 @@ fn route_impl_with_route( .inputs .iter() .map(|arg| match arg { - FnArg::Receiver(_receiver) => panic!("Self type is not supported"), + FnArg::Receiver(_receiver) => _receiver.to_token_stream(), FnArg::Typed(pat_type) => { quote! { #[allow(unused_mut)] @@ -357,31 +365,6 @@ fn route_impl_with_route( } }); - let as_axum_path = route.to_axum_path_string(); - - let query_endpoint = if let Some(route_lit) = route.route_lit.as_ref() { - let prefix = route - .prefix - .as_ref() - .cloned() - .unwrap_or_else(|| LitStr::new("", Span::call_site())) - .value(); - let url_without_queries = route_lit.value().split('?').next().unwrap().to_string(); - let full_url = format!( - "{}{}{}", - prefix, - if url_without_queries.starts_with("/") { - "" - } else { - "/" - }, - url_without_queries - ); - quote! { format!(#full_url, #( #path_param_args)*) } - } else { - quote! { __ENDPOINT_PATH.to_string() } - }; - let endpoint_path = { let prefix = route .prefix @@ -389,6 +372,7 @@ fn route_impl_with_route( .cloned() .unwrap_or_else(|| LitStr::new("", Span::call_site())); + let as_axum_path = route.to_axum_path_string(); let route_lit = if !as_axum_path.is_empty() { quote! { #as_axum_path } } else { @@ -425,6 +409,59 @@ fn route_impl_with_route( } }; + // The endpoint the client will query, passed to `ClientRequest` + // ie `/api/my_fnc` + // + // If there's a `&self` parameter, then we need to look up where `&self` is mounted. + let query_endpoint = if let Some(route_lit) = route.route_lit.as_ref() { + let prefix = route + .prefix + .as_ref() + .cloned() + .unwrap_or_else(|| LitStr::new("", Span::call_site())) + .value(); + let url_without_queries = route_lit.value().split('?').next().unwrap().to_string(); + let full_url = format!( + "{}{}{}", + prefix, + if url_without_queries.starts_with("/") { + "" + } else { + "/" + }, + url_without_queries + ); + quote! { format!(#full_url, #( #path_param_args )*) } + } else { + quote! { __ENDPOINT_PATH.to_string() } + }; + + let receiver = if let Some(self_token) = self_token.take() { + quote! { Self:: } + } else { + quote! {} + }; + + let extract_self = if self_token.is_some() { + quote! { + let __self = ___state.get_endpoint::().expect("Failed to get endpoint state"); + } + } else { + quote! {} + }; + + let self_args = if self_token.is_some() { + quote! { &*__self, } + } else { + quote! {} + }; + + let namespace = if self_token.is_some() { + quote! { Some(std::any::TypeId::of::()) } + } else { + quote! { None } + }; + Ok(quote! { #(#fn_docs)* #route_docs @@ -487,8 +524,6 @@ fn route_impl_with_route( use #__axum::response::IntoResponse; use dioxus_server::ServerFunction; - #function_on_server - #[allow(clippy::unused_unit)] #aide_ident_docs #asyncness fn __inner__function__ #impl_generics( @@ -497,12 +532,14 @@ fn route_impl_with_route( #query_extractor request: #__axum::extract::Request, ) -> Result<#__axum::response::Response, #__axum::response::Response> #where_clause { + #extract_self + let ((#(#server_names,)*), ( #(#body_json_names,)* )) = (&&&&&&&&&&&&&&ServerFnEncoder::<___Body_Serialize___<#(#body_json_types,)*>, (#(#body_json_types,)*)>::new()) .extract_axum(___state.0, request, #unpack).await?; let encoded = (&&&&&&ServerFnDecoder::<#out_ty>::new()) .make_axum_response( - #fn_name #ty_generics(#(#extracted_idents,)* #(#body_json_names,)* #(#server_names,)*).await + #receiver #function_on_server_name #ty_generics(#self_args #(#extracted_idents,)* #(#body_json_names,)* #(#server_names,)*).await ); let response = (&&&&&ServerFnDecoder::<#out_ty>::new()) @@ -515,13 +552,14 @@ fn route_impl_with_route( ServerFunction::new( dioxus_fullstack::http::Method::#method_ident, __ENDPOINT_PATH, - || #__axum::routing::#http_method(__inner__function__ #ty_generics) + || #__axum::routing::#http_method(__inner__function__ #ty_generics), + #namespace ) } #(#server_defaults)* - return #fn_name #ty_generics( + return #function_on_server_name #ty_generics( #(#extracted_idents,)* #(#body_json_names,)* #(#server_names,)* @@ -533,6 +571,10 @@ fn route_impl_with_route( unreachable!() } } + + #[cfg(feature = "server")] + #[doc(hidden)] + #function_on_server }) } @@ -567,10 +609,6 @@ impl CompiledRoute { } PathParam::Static(lit) => path.push_str(&lit.value()), } - // if colon.is_some() { - // path.push(':'); - // } - // path.push_str(&ident.value()); } path @@ -765,7 +803,7 @@ impl CompiledRoute { Some(pat_type.clone()) } else { - unimplemented!("Self type is not supported") + None } }) .collect() @@ -802,7 +840,7 @@ impl CompiledRoute { new_pat_type.pat = Box::new(parse_quote!(#ident)); Some(new_pat_type) } else { - unimplemented!("Self type is not supported") + None } }) .collect() @@ -1214,6 +1252,15 @@ impl PathParam { ty, Brace(span), ) + } else if str.starts_with(':') && str.len() > 1 { + let str = str.strip_prefix(':').unwrap(); + Self::Capture( + LitStr::new(str, span), + Brace(span), + Ident::new(str, span), + ty, + Brace(span), + ) } else { Self::Static(LitStr::new(str, span)) }; diff --git a/packages/fullstack-server/Cargo.toml b/packages/fullstack-server/Cargo.toml index 47b638e989..8a2609af89 100644 --- a/packages/fullstack-server/Cargo.toml +++ b/packages/fullstack-server/Cargo.toml @@ -31,7 +31,7 @@ generational-box = { workspace = true } axum = { workspace = true, features = ["multipart", "ws", "json", "form", "tokio", "http1", "http2", "macros"]} anyhow = { workspace = true } -dashmap = "6.1.0" +dashmap = { workspace = true } inventory = { workspace = true } dioxus-ssr = { workspace = true } diff --git a/packages/fullstack-server/src/serverfn.rs b/packages/fullstack-server/src/serverfn.rs index 926c9fce60..45b8b12f02 100644 --- a/packages/fullstack-server/src/serverfn.rs +++ b/packages/fullstack-server/src/serverfn.rs @@ -5,10 +5,7 @@ use axum::Router; // both req/res // both req/res // req only use dashmap::DashMap; use dioxus_fullstack_core::DioxusServerState; use http::Method; -use std::{marker::PhantomData, sync::LazyLock}; - -pub type AxumRequest = http::Request; -pub type AxumResponse = http::Response; +use std::{any::TypeId, marker::PhantomData, sync::LazyLock}; /// A function endpoint that can be called from the client. #[derive(Clone)] @@ -16,24 +13,23 @@ pub struct ServerFunction { path: &'static str, method: Method, handler: fn() -> MethodRouter, + namespace: Option, _phantom: PhantomData, } -pub struct MakeRequest { - _phantom: PhantomData, -} - impl ServerFunction { /// Create a new server function object. pub const fn new( method: Method, path: &'static str, handler: fn() -> MethodRouter, + namespace: Option, ) -> Self { Self { path, method, handler, + namespace, _phantom: PhantomData, } } diff --git a/packages/fullstack/src/endpoint.rs b/packages/fullstack/src/endpoint.rs new file mode 100644 index 0000000000..b102dd6e0f --- /dev/null +++ b/packages/fullstack/src/endpoint.rs @@ -0,0 +1,24 @@ +//! An endpoint represents an entrypoint for a group of server functions. + +pub struct Endpoint { + path: &'static str, + _marker: std::marker::PhantomData, +} + +impl Endpoint { + /// Create a new endpoint at the given path. + pub const fn new(path: &'static str, f: fn() -> T) -> Self { + Self { + path, + _marker: std::marker::PhantomData, + } + } +} + +impl std::ops::Deref for Endpoint { + type Target = T; + + fn deref(&self) -> &Self::Target { + todo!() + } +} diff --git a/packages/fullstack/src/lib.rs b/packages/fullstack/src/lib.rs index 613d832726..5bb559fb76 100644 --- a/packages/fullstack/src/lib.rs +++ b/packages/fullstack/src/lib.rs @@ -51,6 +51,9 @@ pub use http::{HeaderMap, HeaderValue, Method}; mod client; pub use client::*; +mod endpoint; +pub use endpoint::*; + pub use axum::extract::Json; pub use payloads::*;