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>
This commit is contained in:
authored and GitHub committed 2025-06-26 12:08:02 -05:00
1 parent 0dd0f05db0
commit 6cc379718b
5 files changed
+113 -69

No files matched your search

+3
View File
@@ -9,6 +9,9 @@
"[javascript]": { "[javascript]": {
"editor.formatOnSave": false "editor.formatOnSave": false
}, },
"[html]": {
"editor.formatOnSave": false
},
"dioxus.formatOnSave": "disabled", "dioxus.formatOnSave": "disabled",
// "rust-analyzer.check.workspace": true, // "rust-analyzer.check.workspace": true,
// "rust-analyzer.check.workspace": false, // "rust-analyzer.check.workspace": false,
+1
View File
@@ -1,3 +1,4 @@
<!DOCTYPE html>
<html> <html>
<head> <head>
<title>{app_title}</title> <title>{app_title}</title>
+80 -55
View File
@@ -3364,8 +3364,14 @@ impl BuildRequest {
wasm_opt::optimize(&post_bindgen_wasm, &post_bindgen_wasm, &wasm_opt_options).await?; 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 if self.should_bundle_to_asset() {
// optimizations. // 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() { if self.should_bundle_to_asset() {
// Register the main.js with the asset system so it bundles in the snippets and optimizes // Register the main.js with the asset system so it bundles in the snippets and optimizes
assets.register_asset( assets.register_asset(
@@ -3374,36 +3380,63 @@ 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 // Write the index.html file with the pre-configured contents we got from pre-rendering
self.write_index_html(assets)?; self.write_index_html(assets)?;
Ok(()) 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 /// 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. /// assets are registered with the asset system if this is a release build.
pub(crate) fn write_index_html(&self, assets: &AssetManifest) -> Result<()> { 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_path = self.bundled_wasm_path(assets);
let wasm_bindgen_wasm_out = self.wasm_bindgen_wasm_output_file(); let js_path = self.bundled_js_path(assets);
let wasm_path = 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");
format!("assets/{}", name.bundled_path())
} else {
format!(
"wasm/{}",
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 bundled_js_path(&self, assets: &AssetManifest) -> String {
let wasm_bindgen_js_out = self.wasm_bindgen_js_output_file(); let wasm_bindgen_js_out = self.wasm_bindgen_js_output_file();
let js_path = if self.should_bundle_to_asset() { if self.should_bundle_to_asset() {
let name = assets let name = assets
.get_first_asset_for_source(&wasm_bindgen_js_out) .get_first_asset_for_source(&wasm_bindgen_js_out)
.expect("The js source must exist before creating index.html"); .expect("The js source must exist before creating index.html");
@@ -3413,14 +3446,23 @@ impl BuildRequest {
"wasm/{}", "wasm/{}",
wasm_bindgen_js_out.file_name().unwrap().to_str().unwrap() wasm_bindgen_js_out.file_name().unwrap().to_str().unwrap()
) )
}; }
}
// Write the index.html file with the pre-configured contents we got from pre-rendering /// Get the path to the wasm-bindgen output files. Either the direct file or the opitmized one depending on the build mode
std::fs::write( fn bundled_wasm_path(&self, assets: &AssetManifest) -> String {
self.root_dir().join("index.html"), let wasm_bindgen_wasm_out = self.wasm_bindgen_wasm_output_file();
self.prepare_html(assets, &wasm_path, &js_path).unwrap(), if self.should_bundle_to_asset() {
)?; let name = assets
Ok(()) .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_wasm_out.file_name().unwrap().to_str().unwrap()
)
}
} }
fn info_plist_contents(&self, platform: Platform) -> Result<String> { fn info_plist_contents(&self, platform: Platform) -> Result<String> {
@@ -3908,7 +3950,7 @@ impl BuildRequest {
self.inject_resources(assets, wasm_path, &mut html)?; self.inject_resources(assets, wasm_path, &mut html)?;
// Inject loading scripts if they are not already present // 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 // Replace any special placeholders in the HTML with resolved values
self.replace_template_placeholders(&mut html, wasm_path, js_path); 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 // Manually inject the wasm file for preloading. WASM currently doesn't support preloading in the manganis asset system
head_resources.push_str(&format!( 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); 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 /// Inject loading scripts if they are not already present
fn inject_loading_scripts(&self, html: &mut String) { fn inject_loading_scripts(&self, assets: &AssetManifest, 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 the current build opted out of injecting loading scripts, don't inject anything
if !self.inject_loading_scripts || html.contains("__wbindgen_start") { if !self.inject_loading_scripts {
return; return;
} }
// If not, insert the script // If not, insert the script
*html = html.replace( *html = html.replace(
"</body", "</body",
r#" <script> &format!(
// We can't use a module script here because we need to start the script immediately when streaming r#"<script type="module" async src="/{}/{}"></script>
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>
</body"#, </body"#,
self.base_path_or_default(),
self.bundled_js_path(assets)
),
); );
} }
+1
View File
@@ -58,6 +58,7 @@ features = [
"History", "History",
"HtmlElement", "HtmlElement",
"HtmlFormElement", "HtmlFormElement",
"HtmlHeadElement",
"HtmlInputElement", "HtmlInputElement",
"HtmlSelectElement", "HtmlSelectElement",
"HtmlTextAreaElement", "HtmlTextAreaElement",
+28 -14
View File
@@ -1,8 +1,7 @@
use dioxus_core::prelude::queue_effect; use dioxus_core::prelude::queue_effect;
use dioxus_core::ScopeId; use dioxus_core::ScopeId;
use dioxus_document::{ use dioxus_document::{
create_element_in_head, Document, Eval, EvalError, Evaluator, LinkProps, MetaProps, Document, Eval, EvalError, Evaluator, LinkProps, MetaProps, ScriptProps, StyleProps,
ScriptProps, StyleProps,
}; };
use dioxus_history::History; use dioxus_history::History;
use futures_util::FutureExt; use futures_util::FutureExt;
@@ -97,45 +96,60 @@ impl Document for WebDocument {
/// Create a new meta tag in the head /// Create a new meta tag in the head
fn create_meta(&self, props: MetaProps) { fn create_meta(&self, props: MetaProps) {
let myself = self.clone();
queue_effect(move || { 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 /// Create a new script tag in the head
fn create_script(&self, props: ScriptProps) { fn create_script(&self, props: ScriptProps) {
let myself = self.clone();
queue_effect(move || { queue_effect(move || {
myself.eval(create_element_in_head( append_element_to_head(
"script", "script",
&props.attributes(), &props.attributes(),
props.script_contents().ok(), props.script_contents().ok().as_deref(),
)); );
}); });
} }
/// Create a new style tag in the head /// Create a new style tag in the head
fn create_style(&self, props: StyleProps) { fn create_style(&self, props: StyleProps) {
let myself = self.clone();
queue_effect(move || { queue_effect(move || {
myself.eval(create_element_in_head( append_element_to_head(
"style", "style",
&props.attributes(), &props.attributes(),
props.style_contents().ok(), props.style_contents().ok().as_deref(),
)); );
}); });
} }
/// Create a new link tag in the head /// Create a new link tag in the head
fn create_link(&self, props: LinkProps) { fn create_link(&self, props: LinkProps) {
let myself = self.clone();
queue_effect(move || { 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. /// Required to avoid blocking the Rust WASM thread.
const PROMISE_WRAPPER: &str = r#" const PROMISE_WRAPPER: &str = r#"
return (async function(){ return (async function(){