dioxus_fullstack/
serve_config.rs

1//! Configuration for how to serve a Dioxus application
2#![allow(non_snake_case)]
3
4use std::fs::File;
5use std::io::Read;
6use std::path::PathBuf;
7
8use dioxus_lib::prelude::dioxus_core::LaunchConfig;
9
10use crate::server::ContextProviders;
11
12/// 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`].
13#[derive(Clone, Default)]
14pub struct ServeConfigBuilder {
15    pub(crate) root_id: Option<&'static str>,
16    pub(crate) index_html: Option<String>,
17    pub(crate) index_path: Option<PathBuf>,
18    pub(crate) incremental: Option<dioxus_isrg::IncrementalRendererConfig>,
19    pub(crate) context_providers: ContextProviders,
20    pub(crate) streaming_mode: StreamingMode,
21}
22
23impl LaunchConfig for ServeConfigBuilder {}
24
25impl ServeConfigBuilder {
26    /// Create a new ServeConfigBuilder with incremental static generation disabled and the default index.html settings
27    pub fn new() -> Self {
28        Self {
29            root_id: None,
30            index_html: None,
31            index_path: None,
32            incremental: None,
33            context_providers: Default::default(),
34            streaming_mode: StreamingMode::default(),
35        }
36    }
37
38    /// Enable incremental static generation. Incremental static generation caches the
39    /// rendered html in memory and/or the file system. It can be used to improve performance of heavy routes.
40    ///
41    /// ```rust, no_run
42    /// # fn app() -> Element { todo!() }
43    /// use dioxus::prelude::*;
44    ///
45    /// // Finally, launch the app with the config
46    /// LaunchBuilder::new()
47    ///     // Only set the server config if the server feature is enabled
48    ///     .with_cfg(server_only!(ServeConfigBuilder::default().incremental(IncrementalRendererConfig::default())))
49    ///     .launch(app);
50    /// ```
51    pub fn incremental(mut self, cfg: dioxus_isrg::IncrementalRendererConfig) -> Self {
52        self.incremental = Some(cfg);
53        self
54    }
55
56    /// Set the contents of the index.html file to be served. (precedence over index_path)
57    pub fn index_html(mut self, index_html: String) -> Self {
58        self.index_html = Some(index_html);
59        self
60    }
61
62    /// Set the path of the index.html file to be served. (defaults to {assets_path}/index.html)
63    pub fn index_path(mut self, index_path: PathBuf) -> Self {
64        self.index_path = Some(index_path);
65        self
66    }
67
68    /// Set the id of the root element in the index.html file to place the prerendered content into. (defaults to main)
69    ///
70    /// # Example
71    ///
72    /// If your index.html file looks like this:
73    /// ```html
74    /// <!DOCTYPE html>
75    /// <html>
76    ///     <head>
77    ///         <title>My App</title>
78    ///     </head>
79    ///     <body>
80    ///         <div id="my-custom-root"></div>
81    ///     </body>
82    /// </html>
83    /// ```
84    ///
85    /// You can set the root id to `"my-custom-root"` to render the app into that element:
86    ///
87    /// ```rust, no_run
88    /// # fn app() -> Element { todo!() }
89    /// use dioxus::prelude::*;
90    ///
91    /// // Finally, launch the app with the config
92    /// LaunchBuilder::new()
93    ///     // Only set the server config if the server feature is enabled
94    ///     .with_cfg(server_only! {
95    ///         ServeConfigBuilder::default().root_id("app")
96    ///     })
97    ///     // You also need to set the root id in your web config
98    ///     .with_cfg(web! {
99    ///         dioxus::web::Config::default().rootname("app")
100    ///     })
101    ///     // And desktop config
102    ///     .with_cfg(desktop! {
103    ///         dioxus::desktop::Config::default().with_root_name("app")
104    ///     })
105    ///     .launch(app);
106    /// ```
107    pub fn root_id(mut self, root_id: &'static str) -> Self {
108        self.root_id = Some(root_id);
109        self
110    }
111
112    /// Provide context to the root and server functions. You can use this context
113    /// while rendering with [`consume_context`](dioxus_lib::prelude::consume_context) or in server functions with [`FromContext`](crate::prelude::FromContext).
114    ///
115    /// Context will be forwarded from the LaunchBuilder if it is provided.
116    ///
117    /// ```rust, no_run
118    /// use dioxus::prelude::*;
119    ///
120    /// dioxus::LaunchBuilder::new()
121    ///     // You can provide context to your whole app (including server functions) with the `with_context` method on the launch builder
122    ///     .with_context(server_only! {
123    ///         1234567890u32
124    ///     })
125    ///     .launch(app);
126    ///
127    /// #[server]
128    /// async fn read_context() -> Result<u32, ServerFnError> {
129    ///     // You can extract values from the server context with the `extract` function
130    ///     let FromContext(value) = extract().await?;
131    ///     Ok(value)
132    /// }
133    ///
134    /// fn app() -> Element {
135    ///     let future = use_resource(read_context);
136    ///     rsx! {
137    ///         h1 { "{future:?}" }
138    ///     }
139    /// }
140    /// ```
141    pub fn context_providers(mut self, state: ContextProviders) -> Self {
142        self.context_providers = state;
143        self
144    }
145
146    /// Set the streaming mode for the server. By default, streaming is disabled.
147    ///
148    /// ```rust, no_run
149    /// # use dioxus::prelude::*;
150    /// # fn app() -> Element { todo!() }
151    /// dioxus::LaunchBuilder::new()
152    ///     .with_context(server_only! {
153    ///         dioxus::fullstack::ServeConfig::builder().streaming_mode(dioxus::fullstack::StreamingMode::OutOfOrder)
154    ///     })
155    ///     .launch(app);
156    /// ```
157    pub fn streaming_mode(mut self, mode: StreamingMode) -> Self {
158        self.streaming_mode = mode;
159        self
160    }
161
162    /// Enable out of order streaming. This will cause server futures to be resolved out of order and streamed to the client as they resolve.
163    ///
164    /// It is equivalent to calling `streaming_mode(StreamingMode::OutOfOrder)`
165    ///
166    /// ```rust, no_run
167    /// # use dioxus::prelude::*;
168    /// # fn app() -> Element { todo!() }
169    /// dioxus::LaunchBuilder::new()
170    ///     .with_context(server_only! {
171    ///         dioxus::fullstack::ServeConfig::builder().enable_out_of_order_streaming()
172    ///     })
173    ///     .launch(app);
174    /// ```
175    pub fn enable_out_of_order_streaming(mut self) -> Self {
176        self.streaming_mode = StreamingMode::OutOfOrder;
177        self
178    }
179
180    /// Build the ServeConfig. This may fail if the index.html file is not found.
181    pub fn build(self) -> Result<ServeConfig, UnableToLoadIndex> {
182        // The CLI always bundles static assets into the exe/public directory
183        let public_path = public_path();
184
185        let index_path = self
186            .index_path
187            .map(PathBuf::from)
188            .unwrap_or_else(|| public_path.join("index.html"));
189
190        let root_id = self.root_id.unwrap_or("main");
191
192        let index_html = match self.index_html {
193            Some(index) => index,
194            None => load_index_path(index_path)?,
195        };
196
197        let index = load_index_html(index_html, root_id);
198
199        Ok(ServeConfig {
200            index,
201            incremental: self.incremental,
202            context_providers: self.context_providers,
203            streaming_mode: self.streaming_mode,
204        })
205    }
206}
207
208impl TryInto<ServeConfig> for ServeConfigBuilder {
209    type Error = UnableToLoadIndex;
210
211    fn try_into(self) -> Result<ServeConfig, Self::Error> {
212        self.build()
213    }
214}
215
216/// Get the path to the public assets directory to serve static files from
217pub(crate) fn public_path() -> PathBuf {
218    // The CLI always bundles static assets into the exe/public directory
219    std::env::current_exe()
220        .expect("Failed to get current executable path")
221        .parent()
222        .unwrap()
223        .join("public")
224}
225
226/// An error that can occur when loading the index.html file
227#[derive(Debug)]
228pub struct UnableToLoadIndex(PathBuf);
229
230impl std::fmt::Display for UnableToLoadIndex {
231    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
232        write!(f, "Failed to find index.html. Make sure the index_path is set correctly and the WASM application has been built. Tried to open file at path: {:?}", self.0)
233    }
234}
235
236impl std::error::Error for UnableToLoadIndex {}
237
238fn load_index_path(path: PathBuf) -> Result<String, UnableToLoadIndex> {
239    let mut file = File::open(&path).map_err(|_| UnableToLoadIndex(path))?;
240
241    let mut contents = String::new();
242    file.read_to_string(&mut contents)
243        .expect("Failed to read index.html");
244    Ok(contents)
245}
246
247fn load_index_html(contents: String, root_id: &'static str) -> IndexHtml {
248    let (pre_main, post_main) = contents.split_once(&format!("id=\"{root_id}\"")).unwrap_or_else(|| panic!("Failed to find id=\"{root_id}\" in index.html. The id is used to inject the application into the page."));
249
250    let post_main = post_main.split_once('>').unwrap_or_else(|| {
251        panic!("Failed to find closing > after id=\"{root_id}\" in index.html.")
252    });
253
254    let (pre_main, post_main) = (
255        pre_main.to_string() + &format!("id=\"{root_id}\"") + post_main.0 + ">",
256        post_main.1.to_string(),
257    );
258
259    let (head, close_head) = pre_main.split_once("</head>").unwrap_or_else(|| {
260        panic!("Failed to find closing </head> tag after id=\"{root_id}\" in index.html.")
261    });
262    let (head, close_head) = (head.to_string(), "</head>".to_string() + close_head);
263
264    let (post_main, after_closing_body_tag) =
265        post_main.split_once("</body>").unwrap_or_else(|| {
266            panic!("Failed to find closing </body> tag after id=\"{root_id}\" in index.html.")
267        });
268
269    // Strip out the head if it exists
270    let mut head_before_title = String::new();
271    let mut head_after_title = head;
272    let mut title = String::new();
273    if let Some((new_head_before_title, new_title)) = head_after_title.split_once("<title>") {
274        let (new_title, new_head_after_title) = new_title
275            .split_once("</title>")
276            .expect("Failed to find closing </title> tag after <title> in index.html.");
277        title = format!("<title>{new_title}</title>");
278        head_before_title = new_head_before_title.to_string();
279        head_after_title = new_head_after_title.to_string();
280    }
281
282    IndexHtml {
283        head_before_title,
284        head_after_title,
285        title,
286        close_head,
287        post_main: post_main.to_string(),
288        after_closing_body_tag: "</body>".to_string() + after_closing_body_tag,
289    }
290}
291
292#[derive(Clone)]
293pub(crate) struct IndexHtml {
294    pub(crate) head_before_title: String,
295    pub(crate) head_after_title: String,
296    pub(crate) title: String,
297    pub(crate) close_head: String,
298    pub(crate) post_main: String,
299    pub(crate) after_closing_body_tag: String,
300}
301
302/// The streaming mode to use while rendering the page
303#[derive(Clone, Copy, Default, PartialEq)]
304pub enum StreamingMode {
305    /// Streaming is disabled; all server futures should be resolved before hydrating the page on the client
306    #[default]
307    Disabled,
308    /// Out of order streaming is enabled; server futures are resolved out of order and streamed to the client
309    /// as they resolve
310    OutOfOrder,
311}
312
313/// 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`].
314/// See [`ServeConfigBuilder`] to create a ServeConfig
315#[derive(Clone)]
316pub struct ServeConfig {
317    pub(crate) index: IndexHtml,
318    pub(crate) incremental: Option<dioxus_isrg::IncrementalRendererConfig>,
319    pub(crate) context_providers: ContextProviders,
320    pub(crate) streaming_mode: StreamingMode,
321}
322
323impl LaunchConfig for ServeConfig {}
324
325impl ServeConfig {
326    /// Create a new ServeConfig
327    pub fn new() -> Result<Self, UnableToLoadIndex> {
328        ServeConfigBuilder::new().build()
329    }
330
331    /// Create a new builder for a ServeConfig
332    pub fn builder() -> ServeConfigBuilder {
333        ServeConfigBuilder::new()
334    }
335}