prepare_html.rs 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. //! Build the HTML file to load a web application. The index.html file may be created from scratch or modified from the `index.html` file in the crate root.
  2. use super::{BuildRequest, UpdateBuildProgress};
  3. use crate::builder::progress::MessageSource;
  4. use crate::builder::Stage;
  5. use crate::Result;
  6. use futures_channel::mpsc::UnboundedSender;
  7. use manganis_cli_support::AssetManifest;
  8. use std::fmt::Write;
  9. use std::path::{Path, PathBuf};
  10. use tracing::Level;
  11. const DEFAULT_HTML: &str = include_str!("../../assets/index.html");
  12. const TOAST_HTML: &str = include_str!("../../assets/toast.html");
  13. impl BuildRequest {
  14. pub(crate) fn prepare_html(
  15. &self,
  16. assets: Option<&AssetManifest>,
  17. progress: &mut UnboundedSender<UpdateBuildProgress>,
  18. ) -> Result<String> {
  19. let mut html = html_or_default(&self.dioxus_crate.crate_dir());
  20. // Inject any resources from the config into the html
  21. self.inject_resources(&mut html, assets, progress)?;
  22. // Inject loading scripts if they are not already present
  23. self.inject_loading_scripts(&mut html);
  24. // Replace any special placeholders in the HTML with resolved values
  25. self.replace_template_placeholders(&mut html);
  26. let title = self.dioxus_crate.dioxus_config.web.app.title.clone();
  27. replace_or_insert_before("{app_title}", "</title", &title, &mut html);
  28. Ok(html)
  29. }
  30. // Inject any resources from the config into the html
  31. fn inject_resources(
  32. &self,
  33. html: &mut String,
  34. assets: Option<&AssetManifest>,
  35. progress: &mut UnboundedSender<UpdateBuildProgress>,
  36. ) -> Result<()> {
  37. // Collect all resources into a list of styles and scripts
  38. let resources = &self.dioxus_crate.dioxus_config.web.resource;
  39. let mut style_list = resources.style.clone().unwrap_or_default();
  40. let mut script_list = resources.script.clone().unwrap_or_default();
  41. if self.serve {
  42. style_list.extend(resources.dev.style.iter().cloned());
  43. script_list.extend(resources.dev.script.iter().cloned());
  44. }
  45. let mut head_resources = String::new();
  46. // Add all styles to the head
  47. for style in &style_list {
  48. writeln!(
  49. &mut head_resources,
  50. "<link rel=\"stylesheet\" href=\"{}\">",
  51. &style.to_str().unwrap(),
  52. )?;
  53. }
  54. if !style_list.is_empty() {
  55. self.send_resource_deprecation_warning(progress, style_list, ResourceType::Style);
  56. }
  57. // Add all scripts to the head
  58. for script in &script_list {
  59. writeln!(
  60. &mut head_resources,
  61. "<script src=\"{}\"></script>",
  62. &script.to_str().unwrap(),
  63. )?;
  64. }
  65. if !script_list.is_empty() {
  66. self.send_resource_deprecation_warning(progress, script_list, ResourceType::Script);
  67. }
  68. // Inject any resources from manganis into the head
  69. if let Some(assets) = assets {
  70. head_resources.push_str(&assets.head());
  71. }
  72. replace_or_insert_before("{style_include}", "</head", &head_resources, html);
  73. Ok(())
  74. }
  75. /// Inject loading scripts if they are not already present
  76. fn inject_loading_scripts(&self, html: &mut String) {
  77. // If it looks like we are already loading wasm or the current build opted out of injecting loading scripts, don't inject anything
  78. if !self.build_arguments.inject_loading_scripts || html.contains("__wbindgen_start") {
  79. return;
  80. }
  81. // If not, insert the script
  82. *html = html.replace(
  83. "</body",
  84. r#"<script>
  85. // We can't use a module script here because we need to start the script immediately when streaming
  86. import("/{base_path}/assets/dioxus/{app_name}.js").then(
  87. ({ default: init }) => {
  88. init("/{base_path}/assets/dioxus/{app_name}_bg.wasm").then((wasm) => {
  89. if (wasm.__wbindgen_start == undefined) {
  90. wasm.main();
  91. }
  92. });
  93. }
  94. );
  95. </script>
  96. {DX_TOAST_UTILITIES}
  97. </body"#,
  98. );
  99. *html = match self.serve && !self.build_arguments.release {
  100. true => html.replace("{DX_TOAST_UTILITIES}", TOAST_HTML),
  101. false => html.replace("{DX_TOAST_UTILITIES}", ""),
  102. };
  103. // And try to insert preload links for the wasm and js files
  104. *html = html.replace(
  105. "</head",
  106. r#"<link rel="preload" href="/{base_path}/assets/dioxus/{app_name}_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
  107. <link rel="preload" href="/{base_path}/assets/dioxus/{app_name}.js" as="script">
  108. </head"#);
  109. }
  110. /// Replace any special placeholders in the HTML with resolved values
  111. fn replace_template_placeholders(&self, html: &mut String) {
  112. let base_path = self.dioxus_crate.dioxus_config.web.app.base_path();
  113. *html = html.replace("{base_path}", base_path);
  114. let app_name = &self.dioxus_crate.dioxus_config.application.name;
  115. *html = html.replace("{app_name}", app_name);
  116. }
  117. fn send_resource_deprecation_warning(
  118. &self,
  119. progress: &mut UnboundedSender<UpdateBuildProgress>,
  120. paths: Vec<PathBuf>,
  121. variant: ResourceType,
  122. ) {
  123. const RESOURCE_DEPRECATION_MESSAGE: &str = r#"The `web.resource` config has been deprecated in favor of head components and will be removed in a future release. Instead of including assets in the config, you can include assets with the `asset!` macro and add them to the head with `head::Link` and `Script` components."#;
  124. let replacement_components = paths
  125. .iter()
  126. .map(|path| {
  127. let path = if path.exists() {
  128. path.to_path_buf()
  129. } else {
  130. // If the path is absolute, make it relative to the current directory before we join it
  131. // The path is actually a web path which is relative to the root of the website
  132. let path = path.strip_prefix("/").unwrap_or(path);
  133. let asset_dir_path = self.dioxus_crate.asset_dir().join(path);
  134. if let Ok(absolute_path) = asset_dir_path.canonicalize() {
  135. let absolute_crate_root =
  136. self.dioxus_crate.crate_dir().canonicalize().unwrap();
  137. PathBuf::from("./")
  138. .join(absolute_path.strip_prefix(absolute_crate_root).unwrap())
  139. } else {
  140. path.to_path_buf()
  141. }
  142. };
  143. match variant {
  144. ResourceType::Style => format!(
  145. " head::Link {{ rel: \"stylesheet\", href: asset!(css(\"{}\")) }}",
  146. path.display()
  147. ),
  148. ResourceType::Script => {
  149. format!(" Script {{ src: asset!(file(\"{}\")) }}", path.display())
  150. }
  151. }
  152. })
  153. .collect::<Vec<_>>();
  154. let replacement_components = format!("rsx! {{\n{}\n}}", replacement_components.join("\n"));
  155. let section_name = match variant {
  156. ResourceType::Style => "web.resource.style",
  157. ResourceType::Script => "web.resource.script",
  158. };
  159. let message = format!(
  160. "{RESOURCE_DEPRECATION_MESSAGE}\nTo migrate to head components, remove `{section_name}` and include the following rsx in your root component:\n```rust\n{replacement_components}\n```"
  161. );
  162. _ = progress.unbounded_send(UpdateBuildProgress {
  163. stage: Stage::OptimizingWasm,
  164. update: super::UpdateStage::AddMessage(super::BuildMessage {
  165. level: Level::WARN,
  166. message: super::MessageType::Text(message),
  167. source: MessageSource::Build,
  168. }),
  169. });
  170. }
  171. }
  172. enum ResourceType {
  173. Style,
  174. Script,
  175. }
  176. /// Read the html file from the crate root or use the default html file
  177. fn html_or_default(crate_root: &Path) -> String {
  178. let custom_html_file = crate_root.join("index.html");
  179. std::fs::read_to_string(custom_html_file).unwrap_or_else(|_| String::from(DEFAULT_HTML))
  180. }
  181. /// Replace a string or insert the new contents before a marker
  182. fn replace_or_insert_before(
  183. replace: &str,
  184. or_insert_before: &str,
  185. with: &str,
  186. content: &mut String,
  187. ) {
  188. if content.contains(replace) {
  189. *content = content.replace(replace, with);
  190. } else if let Some(pos) = content.find(or_insert_before) {
  191. content.insert_str(pos, with);
  192. }
  193. }