Forráskód Böngészése

Don't use inline scripts and function constructor (#4310)

* Don't use inline scripts and function constructor.

This allows a stricter Content-Security-Policy which is required for web extensions.

* Load module scripts asynchronously

* Extract common code into method

* Fix injecting loading script

* Append to the file instead of reading and appending to the file string

---------

Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
Co-authored-by: Evan Almloff <evanalmloff@gmail.com>
Moritz Hedtke 23 órája
szülő
commit
6cc379718b

+ 4 - 1
.vscode/settings.json

@@ -9,6 +9,9 @@
   "[javascript]": {
     "editor.formatOnSave": false
   },
+  "[html]": {
+    "editor.formatOnSave": false
+  },
   "dioxus.formatOnSave": "disabled",
   // "rust-analyzer.check.workspace": true,
   // "rust-analyzer.check.workspace": false,
@@ -21,4 +24,4 @@
   "rust-analyzer.cargo.extraArgs": [
     "--tests"
   ],
-}
+}

+ 1 - 0
packages/cli/assets/web/prod.index.html

@@ -1,3 +1,4 @@
+<!DOCTYPE html>
 <html>
     <head>
         <title>{app_title}</title>

+ 79 - 54
packages/cli/src/build/request.rs

@@ -3364,8 +3364,14 @@ impl BuildRequest {
             wasm_opt::optimize(&post_bindgen_wasm, &post_bindgen_wasm, &wasm_opt_options).await?;
         }
 
-        // In release mode, we make the wasm and bindgen files into assets so they get bundled with max
-        // optimizations.
+        if self.should_bundle_to_asset() {
+            // Make sure to register the main wasm file with the asset system
+            assets.register_asset(&post_bindgen_wasm, AssetOptions::Unknown)?;
+        }
+
+        // Now that the wasm is registered as an asset, we can write the js glue shim
+        self.write_js_glue_shim(assets)?;
+
         if self.should_bundle_to_asset() {
             // Register the main.js with the asset system so it bundles in the snippets and optimizes
             assets.register_asset(
@@ -3374,53 +3380,89 @@ impl BuildRequest {
             )?;
         }
 
-        if self.should_bundle_to_asset() {
-            // Make sure to register the main wasm file with the asset system
-            assets.register_asset(&post_bindgen_wasm, AssetOptions::Unknown)?;
-        }
-
         // Write the index.html file with the pre-configured contents we got from pre-rendering
         self.write_index_html(assets)?;
 
         Ok(())
     }
 
+    fn write_js_glue_shim(&self, assets: &AssetManifest) -> Result<()> {
+        let wasm_path = self.bundled_wasm_path(assets);
+
+        // Load and initialize wasm without requiring a separate javascript file.
+        // This also allows using a strict Content-Security-Policy.
+        let mut js = std::fs::OpenOptions::new()
+            .append(true)
+            .open(self.wasm_bindgen_js_output_file())?;
+        let mut buf_writer = std::io::BufWriter::new(&mut js);
+        writeln!(
+            buf_writer,
+            r#"
+window.__wasm_split_main_initSync = initSync;
+
+// Actually perform the load
+__wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{
+    // assign this module to be accessible globally
+    window.__dx_mainWasm = wasm;
+    window.__dx_mainInit = __wbg_init;
+    window.__dx_mainInitSync = initSync;
+    window.__dx___wbg_get_imports = __wbg_get_imports;
+
+    if (wasm.__wbindgen_start == undefined) {{
+        wasm.main();
+    }}
+}});
+"#,
+            self.base_path_or_default(),
+        )?;
+
+        Ok(())
+    }
+
     /// Write the index.html file to the output directory. This must be called after the wasm and js
     /// assets are registered with the asset system if this is a release build.
     pub(crate) fn write_index_html(&self, assets: &AssetManifest) -> Result<()> {
-        // Get the path to the wasm-bindgen output files. Either the direct file or the opitmized one depending on the build mode
-        let wasm_bindgen_wasm_out = self.wasm_bindgen_wasm_output_file();
-        let wasm_path = if self.should_bundle_to_asset() {
+        let wasm_path = self.bundled_wasm_path(assets);
+        let js_path = self.bundled_js_path(assets);
+
+        // Write the index.html file with the pre-configured contents we got from pre-rendering
+        std::fs::write(
+            self.root_dir().join("index.html"),
+            self.prepare_html(assets, &wasm_path, &js_path).unwrap(),
+        )?;
+
+        Ok(())
+    }
+
+    fn bundled_js_path(&self, assets: &AssetManifest) -> String {
+        let wasm_bindgen_js_out = self.wasm_bindgen_js_output_file();
+        if self.should_bundle_to_asset() {
             let name = assets
-                .get_first_asset_for_source(&wasm_bindgen_wasm_out)
-                .expect("The wasm source must exist before creating index.html");
+                .get_first_asset_for_source(&wasm_bindgen_js_out)
+                .expect("The js source must exist before creating index.html");
             format!("assets/{}", name.bundled_path())
         } else {
             format!(
                 "wasm/{}",
-                wasm_bindgen_wasm_out.file_name().unwrap().to_str().unwrap()
+                wasm_bindgen_js_out.file_name().unwrap().to_str().unwrap()
             )
-        };
+        }
+    }
 
-        let wasm_bindgen_js_out = self.wasm_bindgen_js_output_file();
-        let js_path = if self.should_bundle_to_asset() {
+    /// Get the path to the wasm-bindgen output files. Either the direct file or the opitmized one depending on the build mode
+    fn bundled_wasm_path(&self, assets: &AssetManifest) -> String {
+        let wasm_bindgen_wasm_out = self.wasm_bindgen_wasm_output_file();
+        if self.should_bundle_to_asset() {
             let name = assets
-                .get_first_asset_for_source(&wasm_bindgen_js_out)
-                .expect("The js source must exist before creating index.html");
+                .get_first_asset_for_source(&wasm_bindgen_wasm_out)
+                .expect("The wasm source must exist before creating index.html");
             format!("assets/{}", name.bundled_path())
         } else {
             format!(
                 "wasm/{}",
-                wasm_bindgen_js_out.file_name().unwrap().to_str().unwrap()
+                wasm_bindgen_wasm_out.file_name().unwrap().to_str().unwrap()
             )
-        };
-
-        // Write the index.html file with the pre-configured contents we got from pre-rendering
-        std::fs::write(
-            self.root_dir().join("index.html"),
-            self.prepare_html(assets, &wasm_path, &js_path).unwrap(),
-        )?;
-        Ok(())
+        }
     }
 
     fn info_plist_contents(&self, platform: Platform) -> Result<String> {
@@ -3908,7 +3950,7 @@ impl BuildRequest {
         self.inject_resources(assets, wasm_path, &mut html)?;
 
         // Inject loading scripts if they are not already present
-        self.inject_loading_scripts(&mut html);
+        self.inject_loading_scripts(assets, &mut html);
 
         // Replace any special placeholders in the HTML with resolved values
         self.replace_template_placeholders(&mut html, wasm_path, js_path);
@@ -4000,7 +4042,7 @@ impl BuildRequest {
 
         // Manually inject the wasm file for preloading. WASM currently doesn't support preloading in the manganis asset system
         head_resources.push_str(&format!(
-            "<link rel=\"preload\" as=\"fetch\" type=\"application/wasm\" href=\"/{{base_path}}/assets/{wasm_path}\" crossorigin>"
+            "<link rel=\"preload\" as=\"fetch\" type=\"application/wasm\" href=\"/{{base_path}}/{wasm_path}\" crossorigin>"
         ));
         Self::replace_or_insert_before("{style_include}", "</head", &head_resources, html);
 
@@ -4008,38 +4050,21 @@ impl BuildRequest {
     }
 
     /// Inject loading scripts if they are not already present
-    fn inject_loading_scripts(&self, html: &mut String) {
-        // If it looks like we are already loading wasm or the current build opted out of injecting loading scripts, don't inject anything
-        if !self.inject_loading_scripts || html.contains("__wbindgen_start") {
+    fn inject_loading_scripts(&self, assets: &AssetManifest, html: &mut String) {
+        // If the current build opted out of injecting loading scripts, don't inject anything
+        if !self.inject_loading_scripts {
             return;
         }
 
         // If not, insert the script
         *html = html.replace(
             "</body",
-r#" <script>
-  // We can't use a module script here because we need to start the script immediately when streaming
-  import("/{base_path}/{js_path}").then(
-    ({ default: init, initSync, __wbg_get_imports }) => {
-      // export initSync in case a split module needs to initialize
-      window.__wasm_split_main_initSync = initSync;
-
-      // Actually perform the load
-      init({module_or_path: "/{base_path}/{wasm_path}"}).then((wasm) => {
-        // assign this module to be accessible globally
-        window.__dx_mainWasm = wasm;
-        window.__dx_mainInit = init;
-        window.__dx_mainInitSync = initSync;
-        window.__dx___wbg_get_imports = __wbg_get_imports;
-
-        if (wasm.__wbindgen_start == undefined) {
-            wasm.main();
-        }
-      });
-    }
-  );
-  </script>
+            &format!(
+                r#"<script type="module" async src="/{}/{}"></script>
             </body"#,
+                self.base_path_or_default(),
+                self.bundled_js_path(assets)
+            ),
         );
     }
 

+ 1 - 0
packages/web/Cargo.toml

@@ -58,6 +58,7 @@ features = [
     "History",
     "HtmlElement",
     "HtmlFormElement",
+    "HtmlHeadElement",
     "HtmlInputElement",
     "HtmlSelectElement",
     "HtmlTextAreaElement",

+ 28 - 14
packages/web/src/document.rs

@@ -1,8 +1,7 @@
 use dioxus_core::prelude::queue_effect;
 use dioxus_core::ScopeId;
 use dioxus_document::{
-    create_element_in_head, Document, Eval, EvalError, Evaluator, LinkProps, MetaProps,
-    ScriptProps, StyleProps,
+    Document, Eval, EvalError, Evaluator, LinkProps, MetaProps, ScriptProps, StyleProps,
 };
 use dioxus_history::History;
 use futures_util::FutureExt;
@@ -97,45 +96,60 @@ impl Document for WebDocument {
 
     /// Create a new meta tag in the head
     fn create_meta(&self, props: MetaProps) {
-        let myself = self.clone();
         queue_effect(move || {
-            myself.eval(create_element_in_head("meta", &props.attributes(), None));
+            append_element_to_head("meta", &props.attributes(), None);
         });
     }
 
     /// Create a new script tag in the head
     fn create_script(&self, props: ScriptProps) {
-        let myself = self.clone();
         queue_effect(move || {
-            myself.eval(create_element_in_head(
+            append_element_to_head(
                 "script",
                 &props.attributes(),
-                props.script_contents().ok(),
-            ));
+                props.script_contents().ok().as_deref(),
+            );
         });
     }
 
     /// Create a new style tag in the head
     fn create_style(&self, props: StyleProps) {
-        let myself = self.clone();
         queue_effect(move || {
-            myself.eval(create_element_in_head(
+            append_element_to_head(
                 "style",
                 &props.attributes(),
-                props.style_contents().ok(),
-            ));
+                props.style_contents().ok().as_deref(),
+            );
         });
     }
 
     /// Create a new link tag in the head
     fn create_link(&self, props: LinkProps) {
-        let myself = self.clone();
         queue_effect(move || {
-            myself.eval(create_element_in_head("link", &props.attributes(), None));
+            append_element_to_head("link", &props.attributes(), None);
         });
     }
 }
 
+fn append_element_to_head(
+    local_name: &str,
+    attributes: &Vec<(&'static str, String)>,
+    text_content: Option<&str>,
+) {
+    let window = web_sys::window().expect("no global `window` exists");
+    let document = window.document().expect("should have a document on window");
+    let head = document.head().expect("document should have a head");
+
+    let element = document.create_element(local_name).unwrap();
+    for (name, value) in attributes {
+        element.set_attribute(name, value).unwrap();
+    }
+    if text_content.is_some() {
+        element.set_text_content(text_content);
+    }
+    head.append_child(&element).unwrap();
+}
+
 /// Required to avoid blocking the Rust WASM thread.
 const PROMISE_WRAPPER: &str = r#"
     return (async function(){