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}