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