web.rs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. use dioxus_cli_config::format_base_path_meta_element;
  2. use manganis::AssetOptions;
  3. use crate::error::Result;
  4. use std::fmt::Write;
  5. use std::path::{Path, PathBuf};
  6. use super::AppBundle;
  7. const DEFAULT_HTML: &str = include_str!("../../assets/web/index.html");
  8. const TOAST_HTML: &str = include_str!("../../assets/web/toast.html");
  9. impl AppBundle {
  10. pub(crate) fn prepare_html(&self) -> Result<String> {
  11. let mut html = {
  12. let crate_root: &Path = &self.build.krate.crate_dir();
  13. let custom_html_file = crate_root.join("index.html");
  14. std::fs::read_to_string(custom_html_file).unwrap_or_else(|_| String::from(DEFAULT_HTML))
  15. };
  16. // Inject any resources from the config into the html
  17. self.inject_resources(&mut html)?;
  18. // Inject loading scripts if they are not already present
  19. self.inject_loading_scripts(&mut html);
  20. // Replace any special placeholders in the HTML with resolved values
  21. self.replace_template_placeholders(&mut html);
  22. let title = self.build.krate.config.web.app.title.clone();
  23. replace_or_insert_before("{app_title}", "</title", &title, &mut html);
  24. Ok(html)
  25. }
  26. fn is_dev_build(&self) -> bool {
  27. !self.build.build.release
  28. }
  29. // Inject any resources from the config into the html
  30. fn inject_resources(&self, html: &mut String) -> Result<()> {
  31. // Collect all resources into a list of styles and scripts
  32. let resources = &self.build.krate.config.web.resource;
  33. let mut style_list = resources.style.clone().unwrap_or_default();
  34. let mut script_list = resources.script.clone().unwrap_or_default();
  35. if self.is_dev_build() {
  36. style_list.extend(resources.dev.style.iter().cloned());
  37. script_list.extend(resources.dev.script.iter().cloned());
  38. }
  39. let mut head_resources = String::new();
  40. // Add all styles to the head
  41. for style in &style_list {
  42. writeln!(
  43. &mut head_resources,
  44. "<link rel=\"stylesheet\" href=\"{}\">",
  45. &style.to_str().unwrap(),
  46. )?;
  47. }
  48. // Add all scripts to the head
  49. for script in &script_list {
  50. writeln!(
  51. &mut head_resources,
  52. "<script src=\"{}\"></script>",
  53. &script.to_str().unwrap(),
  54. )?;
  55. }
  56. // Add the base path to the head if this is a debug build
  57. if self.is_dev_build() {
  58. if let Some(base_path) = &self.build.krate.config.web.app.base_path {
  59. head_resources.push_str(&format_base_path_meta_element(base_path));
  60. }
  61. }
  62. if !style_list.is_empty() {
  63. self.send_resource_deprecation_warning(style_list, ResourceType::Style);
  64. }
  65. if !script_list.is_empty() {
  66. self.send_resource_deprecation_warning(script_list, ResourceType::Script);
  67. }
  68. // Inject any resources from manganis into the head
  69. for asset in self.app.assets.assets.values() {
  70. let asset_path = asset.bundled_path();
  71. match asset.options() {
  72. AssetOptions::Css(css_options) => {
  73. if css_options.preloaded() {
  74. head_resources.push_str(&format!(
  75. "<link rel=\"preload\" as=\"style\" href=\"/{{base_path}}/assets/{asset_path}\" crossorigin>"
  76. ))
  77. }
  78. }
  79. AssetOptions::Image(image_options) => {
  80. if image_options.preloaded() {
  81. head_resources.push_str(&format!(
  82. "<link rel=\"preload\" as=\"image\" href=\"/{{base_path}}/assets/{asset_path}\" crossorigin>"
  83. ))
  84. }
  85. }
  86. AssetOptions::Js(js_options) => {
  87. if js_options.preloaded() {
  88. head_resources.push_str(&format!(
  89. "<link rel=\"preload\" as=\"script\" href=\"/{{base_path}}/assets/{asset_path}\" crossorigin>"
  90. ))
  91. }
  92. }
  93. _ => {}
  94. }
  95. }
  96. // Manually inject the wasm file for preloading. WASM currently doesn't support preloading in the manganis asset system
  97. let wasm_source_path = self.build.wasm_bindgen_wasm_output_file();
  98. let wasm_path = self
  99. .app
  100. .assets
  101. .assets
  102. .get(&wasm_source_path)
  103. .expect("WASM asset should exist in web bundles")
  104. .bundled_path();
  105. head_resources.push_str(&format!(
  106. "<link rel=\"preload\" as=\"fetch\" type=\"application/wasm\" href=\"/{{base_path}}/assets/{wasm_path}\" crossorigin>"
  107. ));
  108. replace_or_insert_before("{style_include}", "</head", &head_resources, html);
  109. Ok(())
  110. }
  111. /// Inject loading scripts if they are not already present
  112. fn inject_loading_scripts(&self, html: &mut String) {
  113. // If it looks like we are already loading wasm or the current build opted out of injecting loading scripts, don't inject anything
  114. if !self.build.build.inject_loading_scripts || html.contains("__wbindgen_start") {
  115. return;
  116. }
  117. // If not, insert the script
  118. *html = html.replace(
  119. "</body",
  120. r#" <script>
  121. // We can't use a module script here because we need to start the script immediately when streaming
  122. import("/{base_path}/{js_path}").then(
  123. ({ default: init, initSync }) => {
  124. // export initSync in case a split module needs to initialize
  125. window.__wasm_split_main_initSync = initSync;
  126. // Actually perform the load
  127. init({module_or_path: "/{base_path}/{wasm_path}"}).then((wasm) => {
  128. if (wasm.__wbindgen_start == undefined) {
  129. wasm.main();
  130. }
  131. });
  132. }
  133. );
  134. </script>
  135. {DX_TOAST_UTILITIES}
  136. </body"#,
  137. );
  138. // Trim out the toasts if we're in release, or add them if we're serving
  139. *html = match self.is_dev_build() {
  140. true => html.replace("{DX_TOAST_UTILITIES}", TOAST_HTML),
  141. false => html.replace("{DX_TOAST_UTILITIES}", ""),
  142. };
  143. }
  144. /// Replace any special placeholders in the HTML with resolved values
  145. fn replace_template_placeholders(&self, html: &mut String) {
  146. let base_path = self.build.krate.config.web.app.base_path();
  147. *html = html.replace("{base_path}", base_path);
  148. let app_name = &self.build.krate.executable_name();
  149. let wasm_source_path = self.build.wasm_bindgen_wasm_output_file();
  150. let wasm_path = self
  151. .app
  152. .assets
  153. .assets
  154. .get(&wasm_source_path)
  155. .expect("WASM asset should exist in web bundles")
  156. .bundled_path();
  157. let wasm_path = format!("assets/{wasm_path}");
  158. let js_source_path = self.build.wasm_bindgen_js_output_file();
  159. let js_path = self
  160. .app
  161. .assets
  162. .assets
  163. .get(&js_source_path)
  164. .expect("JS asset should exist in web bundles")
  165. .bundled_path();
  166. let js_path = format!("assets/{js_path}");
  167. // If the html contains the old `{app_name}` placeholder, replace {app_name}_bg.wasm and {app_name}.js
  168. // with the new paths
  169. *html = html.replace("wasm/{app_name}_bg.wasm", &wasm_path);
  170. *html = html.replace("wasm/{app_name}.js", &js_path);
  171. // Otherwise replace the new placeholders
  172. *html = html.replace("{wasm_path}", &wasm_path);
  173. *html = html.replace("{js_path}", &js_path);
  174. // Replace the app_name if we find it anywhere standalone
  175. *html = html.replace("{app_name}", app_name);
  176. }
  177. fn send_resource_deprecation_warning(&self, paths: Vec<PathBuf>, variant: ResourceType) {
  178. 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 `document::Link` and `Script` components."#;
  179. let replacement_components = paths
  180. .iter()
  181. .map(|path| {
  182. let path = if path.exists() {
  183. path.to_path_buf()
  184. } else {
  185. // If the path is absolute, make it relative to the current directory before we join it
  186. // The path is actually a web path which is relative to the root of the website
  187. let path = path.strip_prefix("/").unwrap_or(path);
  188. let asset_dir_path = self
  189. .build
  190. .krate
  191. .legacy_asset_dir()
  192. .map(|dir| dir.join(path).canonicalize());
  193. if let Some(Ok(absolute_path)) = asset_dir_path {
  194. let absolute_crate_root =
  195. self.build.krate.crate_dir().canonicalize().unwrap();
  196. PathBuf::from("./")
  197. .join(absolute_path.strip_prefix(absolute_crate_root).unwrap())
  198. } else {
  199. path.to_path_buf()
  200. }
  201. };
  202. match variant {
  203. ResourceType::Style => {
  204. format!(" Stylesheet {{ href: asset!(\"{}\") }}", path.display())
  205. }
  206. ResourceType::Script => {
  207. format!(" Script {{ src: asset!(\"{}\") }}", path.display())
  208. }
  209. }
  210. })
  211. .collect::<Vec<_>>();
  212. let replacement_components = format!("rsx! {{\n{}\n}}", replacement_components.join("\n"));
  213. let section_name = match variant {
  214. ResourceType::Style => "web.resource.style",
  215. ResourceType::Script => "web.resource.script",
  216. };
  217. tracing::warn!(
  218. "{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```"
  219. );
  220. }
  221. }
  222. enum ResourceType {
  223. Style,
  224. Script,
  225. }
  226. /// Replace a string or insert the new contents before a marker
  227. fn replace_or_insert_before(
  228. replace: &str,
  229. or_insert_before: &str,
  230. with: &str,
  231. content: &mut String,
  232. ) {
  233. if content.contains(replace) {
  234. *content = content.replace(replace, with);
  235. } else if let Some(pos) = content.find(or_insert_before) {
  236. content.insert_str(pos, with);
  237. }
  238. }