serve_config.rs 12 KB

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