From 306639ee9feacedce22047af65c28e012ce955e1 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 20 Oct 2025 12:22:35 -0400 Subject: [PATCH] follow up to serveconfig (#4820) * remove latent dbg! * cleanup serveconfig --- packages/fullstack-server/src/config.rs | 133 +++++++----------- packages/fullstack-server/src/index_html.rs | 47 ++++++- packages/fullstack-server/src/server.rs | 26 ++-- packages/fullstack-server/src/ssr.rs | 12 +- .../fullstack-spread/src/main.rs | 21 ++- 5 files changed, 120 insertions(+), 119 deletions(-) diff --git a/packages/fullstack-server/src/config.rs b/packages/fullstack-server/src/config.rs index e3739385e..b1c79651b 100644 --- a/packages/fullstack-server/src/config.rs +++ b/packages/fullstack-server/src/config.rs @@ -11,11 +11,9 @@ use crate::{IncrementalRendererConfig, IndexHtml}; pub(crate) type ContextProviders = Arc Box + Send + Sync + 'static>>>; /// A ServeConfig is used to configure how to serve a Dioxus application. It contains information about how to serve static assets, and what content to render with [`dioxus_ssr`]. -#[derive(Clone, Default)] +#[derive(Clone)] pub struct ServeConfig { - pub(crate) root_id: Option<&'static str>, - pub(crate) index: Option, - pub(crate) index_html_override: Option, + pub(crate) index: IndexHtml, pub(crate) incremental: Option, pub(crate) context_providers: Vec Box + Send + Sync + 'static>>, pub(crate) streaming_mode: StreamingMode, @@ -35,6 +33,12 @@ pub enum StreamingMode { impl LaunchConfig for ServeConfig {} +impl Default for ServeConfig { + fn default() -> Self { + Self::new() + } +} + impl ServeConfig { /// Create a new ServeConfig with incremental static generation disabled and the default index.html settings. pub fn builder() -> Self { @@ -42,17 +46,54 @@ impl ServeConfig { } /// Create a new ServeConfig with incremental static generation disabled and the default index.html settings + /// + /// This will automatically use the `index.html` file in the `/public` directory if it exists. + /// The `/public` folder is meant located next to the current executable. If no `index.html` file is found, + /// a default index.html will be used, which will not include any JavaScript or WASM initialization code. + /// + /// To provide an alternate `index.html`, you can use `with_index_html` method instead. pub fn new() -> Self { + let index = if let Some(public_path) = crate::public_path() { + let index_html_path = public_path.join("index.html"); + + if index_html_path.exists() { + let index_html = std::fs::read_to_string(index_html_path) + .expect("Failed to read index.html from public directory"); + + IndexHtml::new(&index_html, "main") + .expect("Failed to parse index.html from public directory") + } else { + tracing::warn!("No index.html found in public directory, using default index.html"); + IndexHtml::ssr_only() + } + } else { + tracing::warn!( + "Cannot identify public directory, using default index.html. If you need client-side scripts (like JS + WASM), please provide an explicit public directory." + ); + IndexHtml::ssr_only() + }; + Self { - root_id: None, + index, incremental: None, - index: None, - index_html_override: None, context_providers: Default::default(), streaming_mode: StreamingMode::default(), } } + /// Create a new ServeConfig with the given parsed `IndexHtml` structure. + /// + /// You can create the `IndexHtml` structure by using `IndexHtml::new` method, or manually from + /// a string or file. + pub fn with_index_html(index: IndexHtml) -> Self { + Self { + index, + incremental: Default::default(), + context_providers: Default::default(), + streaming_mode: Default::default(), + } + } + /// Enable incremental static generation. Incremental static generation caches the /// rendered html in memory and/or the file system. It can be used to improve performance of heavy routes. /// @@ -71,56 +112,6 @@ impl ServeConfig { self } - /// Set the contents of the index.html file to be served. - pub fn index_html(mut self, index_html: String) -> Self { - self.index_html_override = Some(index_html); - self - } - - /// Set the id of the root element in the index.html file to place the prerendered content into. (defaults to main) - /// - /// # Example - /// - /// If your index.html file looks like this: - /// ```html - /// - /// - /// - /// My App - /// - /// - ///
- /// - /// - /// ``` - /// - /// You can set the root id to `"my-custom-root"` to render the app into that element: - /// - /// ```rust, no_run - /// # fn app() -> Element { unimplemented!() } - /// use dioxus::prelude::*; - /// - /// // Finally, launch the app with the config - /// LaunchBuilder::new() - /// // Only set the server config if the server feature is enabled - /// .with_cfg(server_only! { - /// dioxus_server::ServeConfig::default().root_id("app") - /// }) - /// // You also need to set the root id in your web config - /// .with_cfg(web! { - /// dioxus::web::Config::default().rootname("app") - /// }) - /// // And desktop config - /// .with_cfg(desktop! { - /// dioxus::desktop::Config::default().with_root_name("app") - /// }) - /// .launch(app); - /// ``` - pub fn root_id(mut self, root_id: &'static str) -> Self { - self.root_id = Some(root_id); - self - } - /// Provide context to the root and server functions. You can use this context while rendering with [`consume_context`](dioxus_core::consume_context). /// /// @@ -313,32 +304,4 @@ impl ServeConfig { self.streaming_mode = StreamingMode::OutOfOrder; self } - - pub(crate) fn resolve_index(&mut self) -> &IndexHtml { - // Create the IndexHtml object if the user has specified an override - if let Some(raw_index_html) = self.index_html_override.as_ref() { - self.index = Some( - IndexHtml::new(raw_index_html, self.root_id.unwrap_or("main")) - .expect("Failed to parse custom index.html"), - ); - } else if !cfg!(target_arch = "wasm32") { - let public_path = crate::public_path(); - let index_html_path = public_path.join("index.html"); - - if index_html_path.exists() { - let index_html = std::fs::read_to_string(index_html_path) - .expect("Failed to read index.html from public directory"); - self.index = Some( - IndexHtml::new(&index_html, self.root_id.unwrap_or("main")) - .expect("Failed to parse index.html from public directory"), - ); - } - } else if self.index.is_none() { - self.index = Some(IndexHtml::default()); - } - - self.index - .as_ref() - .expect("IndexHtml is always set after it is resolved") - } } diff --git a/packages/fullstack-server/src/index_html.rs b/packages/fullstack-server/src/index_html.rs index 5f4147b89..bde3f5530 100644 --- a/packages/fullstack-server/src/index_html.rs +++ b/packages/fullstack-server/src/index_html.rs @@ -1,11 +1,34 @@ use anyhow::Context; +use std::path::Path; /// An `IndexHtml` represents the contents of an `index.html` file used to serve a web application. /// /// This defines the static portion of your web application, typically generated by a tool like `dx` /// in conjunction with wasm-bindgen. +/// +/// This structure expects a well-formed HTML document split into several sections: +/// - `head_before_title`: The portion of the `` section before the `` tag and its contents. +/// - `head_after_title`: The portion of the `<head>` section after the `<title>` tag. +/// - `close_head`: The closing `</head>` tag and any content following it. +/// - `post_main`: The content following the main application container (e.g., `<div id="main">`). +/// - `after_closing_body_tag`: The content following the closing `</body>` tag. +/// +/// These fields are not explicitly exposed as part of the API, but are critical for dioxus-fullstack +/// to properly inject server-rendered content and client-side bootstrapping scripts into the HTML. +/// +/// The simplest HTML document that satisfies this structure is: +/// +/// ```html +/// <html> +/// <head> </head> +/// <body> +/// <div id="main"></div> +/// </body> +/// </html> +/// ``` #[derive(Clone, Debug)] -pub(crate) struct IndexHtml { +pub struct IndexHtml { pub(crate) head_before_title: String, pub(crate) head_after_title: String, pub(crate) title: String, @@ -20,7 +43,7 @@ impl IndexHtml { /// This function will parse the `index.html` and split it into sections for easier manipulation. /// The `root_id` parameter specifies the id of the main application container (e.g., "main") /// which your app will render into. - pub(crate) fn new(contents: &str, root_id: &'static str) -> Result<IndexHtml, anyhow::Error> { + pub fn new(contents: &str, root_id: &str) -> Result<IndexHtml, anyhow::Error> { let (pre_main, post_main) = contents.split_once(&format!("id=\"{root_id}\"")) .with_context(|| format!("Failed to find id=\"{root_id}\" in index.html. The id is used to inject the application into the page."))?; @@ -57,19 +80,29 @@ impl IndexHtml { head_after_title = new_head_after_title.to_string(); } - Ok(dbg!(IndexHtml { + Ok(IndexHtml { head_before_title, head_after_title, title, close_head, post_main: post_main.to_string(), after_closing_body_tag: "</body>".to_string() + after_closing_body_tag, - })) + }) } -} -impl Default for IndexHtml { - fn default() -> Self { + /// Load an `IndexHtml` from a file at the given path. + /// + /// This function reads the contents of the file and parses it into an `IndexHtml`. + /// The `id` parameter specifies the id of the main application container (e.g., "main"). + pub fn from_file(path: &Path, id: &str) -> Result<IndexHtml, anyhow::Error> { + let contents = std::fs::read_to_string(path) + .with_context(|| format!("Failed to read index.html from {}", path.display()))?; + IndexHtml::new(&contents, id) + } + + /// Create a default `IndexHtml` suitable for server-side rendering (SSR) only applications. + /// This will not include any customizations to the HTML nor any JavaScript to bootstrap a client-side app. + pub fn ssr_only() -> Self { const DEFAULT: &str = r#"<!DOCTYPE html> <html> <head> </head> diff --git a/packages/fullstack-server/src/server.rs b/packages/fullstack-server/src/server.rs index d2ccb3f6d..ad55b3d8b 100644 --- a/packages/fullstack-server/src/server.rs +++ b/packages/fullstack-server/src/server.rs @@ -120,11 +120,9 @@ where S: Send + Sync + Clone + 'static, { fn serve_static_assets(self) -> Self { - let public_path = public_path(); - - if !public_path.exists() { + let Some(public_path) = public_path() else { return self; - } + }; // Serve all files in public folder except index.html serve_dir_cached(self, &public_path, &public_path) @@ -284,11 +282,9 @@ impl RenderHandleState { /// axum::serve(listener, router).await.unwrap(); /// } /// ``` - pub async fn render_handler(State(mut state): State<Self>, request: Request<Body>) -> Response { + pub async fn render_handler(State(state): State<Self>, request: Request<Body>) -> Response { let (parts, _) = request.into_parts(); - state.config.resolve_index(); - let response = state .renderers .clone() @@ -350,17 +346,19 @@ impl RenderHandleState { } /// Get the path to the public assets directory to serve static files from -pub(crate) fn public_path() -> PathBuf { +pub(crate) fn public_path() -> Option<PathBuf> { if let Ok(path) = std::env::var("DIOXUS_PUBLIC_PATH") { - return PathBuf::from(path); + return Some(PathBuf::from(path)); } // The CLI always bundles static assets into the exe/public directory - std::env::current_exe() - .expect("Failed to get current executable path") - .parent() - .unwrap() - .join("public") + Some( + std::env::current_exe() + .ok()? + .parent() + .unwrap() + .join("public"), + ) } fn serve_dir_cached<S>(mut router: Router<S>, public_path: &Path, directory: &Path) -> Router<S> diff --git a/packages/fullstack-server/src/ssr.rs b/packages/fullstack-server/src/ssr.rs index e1f18ddd6..3f13a46de 100644 --- a/packages/fullstack-server/src/ssr.rs +++ b/packages/fullstack-server/src/ssr.rs @@ -680,13 +680,13 @@ impl SsrRendererPool { document.and_then(|document| document.title()) }; - to.write_str(&cfg.index.as_ref().unwrap().head_before_title)?; + to.write_str(&cfg.index.head_before_title)?; if let Some(title) = title { to.write_str(&title)?; } else { - to.write_str(&cfg.index.as_ref().unwrap().title)?; + to.write_str(&cfg.index.title)?; } - to.write_str(&cfg.index.as_ref().unwrap().head_after_title)?; + to.write_str(&cfg.index.head_after_title)?; let document = virtual_dom.in_scope(ScopeId::ROOT, try_consume_context::<Rc<ServerDocument>>); @@ -708,7 +708,7 @@ impl SsrRendererPool { cfg: &ServeConfig, to: &mut R, ) -> Result<(), IncrementalRendererError> { - to.write_str(&cfg.index.as_ref().unwrap().close_head)?; + to.write_str(&cfg.index.close_head)?; // // #[cfg(feature = "document")] // { @@ -749,7 +749,7 @@ impl SsrRendererPool { )?; } write!(to, r#"</script>"#,)?; - to.write_str(&cfg.index.as_ref().unwrap().post_main)?; + to.write_str(&cfg.index.post_main)?; Ok(()) } @@ -759,7 +759,7 @@ impl SsrRendererPool { cfg: &ServeConfig, to: &mut R, ) -> Result<(), IncrementalRendererError> { - to.write_str(&cfg.index.as_ref().unwrap().after_closing_body_tag)?; + to.write_str(&cfg.index.after_closing_body_tag)?; Ok(()) } diff --git a/packages/playwright-tests/fullstack-spread/src/main.rs b/packages/playwright-tests/fullstack-spread/src/main.rs index 0fe091c84..0ac51eef7 100644 --- a/packages/playwright-tests/fullstack-spread/src/main.rs +++ b/packages/playwright-tests/fullstack-spread/src/main.rs @@ -3,13 +3,20 @@ use dioxus::prelude::*; fn main() { - dioxus::launch(|| { - rsx! { - Comp {} - Comp {} - Button {} - } - }); + // we split these two to ensure `dioxus::serve` works properly. + #[cfg(feature = "server")] + dioxus::serve(|| async move { Ok(dioxus::server::router(app)) }); + + #[cfg(not(feature = "server"))] + dioxus::launch(app); +} + +fn app() -> Element { + rsx! { + Comp {} + Comp {} + Button {} + } } #[component]