@@ -36,7 +36,7 @@ use std::{
3636 str:: FromStr ,
3737 sync:: Arc ,
3838} ;
39- use tokio:: { fs, io :: AsyncWriteExt , process:: Command , sync:: mpsc, task:: JoinHandle } ;
39+ use tokio:: { fs, process:: Command , sync:: mpsc, task:: JoinHandle } ;
4040use tracing:: log;
4141use wasm_bindgen:: { WasmBindgenFeatures , WasmBindgenTarget , find_wasm_bindgen_version} ;
4242use wasm_opt:: WasmOptLevel ;
@@ -607,29 +607,33 @@ impl RustApp {
607607
608608 // Copy the generated WASM & JS loader to the dist dir.
609609 tracing:: debug!( "copying generated wasm-bindgen artifacts" ) ;
610- let hashed_name = self . hashed_wasm_base ( wasm_path) . await ?;
611- let hashed_wasm_name =
612- apply_data_target_path ( format ! ( "{hashed_name}_bg.wasm" ) , & self . target_path ) ;
613610
614611 let js_name = format ! ( "{}.js" , self . name) ;
615- let hashed_js_name = apply_data_target_path ( format ! ( "{hashed_name}.js" ) , & self . target_path ) ;
612+ let js_loader_path = bindgen_out. join ( & js_name) ;
613+ let wasm_name = format ! ( "{}_bg.wasm" , self . name) ;
614+ let wasm_path = bindgen_out. join ( & wasm_name) ;
615+
616+ // Hash each artifact by its own wasm-bindgen output, so a file's hashed name changes
617+ // exactly when that file's bytes change and the name stays in lockstep with the SRI
618+ // integrity computed from those bytes (see trunk-rs/trunk#1028). wasm-bindgen normalizes
619+ // the non-deterministic cargo wasm into deterministic output, so hashing the cargo input
620+ // instead would churn the wasm file name on every build even when the served wasm is
621+ // identical; and its JS output is itself non-deterministic for byte-identical wasm, so
622+ // keying the JS name off the wasm would decouple the name from the bytes SRI pins.
623+ let hashed_wasm_base = self . hashed_base ( wasm_path. as_std_path ( ) ) . await ?;
624+ let hashed_js_base = self . hashed_base ( js_loader_path. as_std_path ( ) ) . await ?;
625+
626+ let hashed_wasm_name =
627+ apply_data_target_path ( format ! ( "{hashed_wasm_base}_bg.wasm" ) , & self . target_path ) ;
628+ let hashed_js_name =
629+ apply_data_target_path ( format ! ( "{hashed_js_base}.js" ) , & self . target_path ) ;
616630 let ts_name = format ! ( "{}.d.ts" , self . name) ;
617631 let hashed_ts_name =
618- apply_data_target_path ( format ! ( "{hashed_name }.d.ts" ) , & self . target_path ) ;
632+ apply_data_target_path ( format ! ( "{hashed_js_base }.d.ts" ) , & self . target_path ) ;
619633
620- let js_loader_path = bindgen_out. join ( & js_name) ;
621634 let js_loader_path_dist = self . cfg . staging_dist . join ( & hashed_js_name) ;
622- let wasm_name = format ! ( "{}_bg.wasm" , self . name) ;
623- let wasm_path = bindgen_out. join ( & wasm_name) ;
624635 let wasm_path_dist = self . cfg . staging_dist . join ( & hashed_wasm_name) ;
625636
626- let hashed_loader_name = self
627- . loader_shim
628- . then ( || apply_data_target_path ( format ! ( "{hashed_name}_loader.js" ) , & self . target_path ) ) ;
629- let loader_shim_path = hashed_loader_name
630- . as_ref ( )
631- . map ( |m| self . cfg . staging_dist . join ( m) ) ;
632-
633637 tracing:: debug!(
634638 "copying {js_loader_path} to {}" ,
635639 js_loader_path_dist. display( )
@@ -661,12 +665,7 @@ impl RustApp {
661665 . context ( "error copying TS files to stage dir" ) ?;
662666 }
663667
664- if let Some ( ref m) = loader_shim_path {
665- tracing:: debug!( "creating {}" , m. display( ) ) ;
666- let mut loader_f = fs:: File :: create ( m)
667- . await
668- . context ( "error creating loader shim script" ) ?;
669-
668+ if self . loader_shim {
670669 let shim = match self . wasm_bindgen_target {
671670 WasmBindgenTarget :: Web => {
672671 format ! ( "import init from './{hashed_js_name}';await init();" )
@@ -679,14 +678,26 @@ impl RustApp {
679678 \" no-modules\" !"
680679 ) ,
681680 } ;
682- loader_f
683- . write_all ( shim. as_bytes ( ) )
681+
682+ // Hash the loader by its own content so its file name stays in lockstep with the
683+ // js/wasm names it embeds; keying it off either file's base alone would leave a
684+ // stale loader (404) when only the other file changed (see trunk-rs/trunk#1028).
685+ let loader_src = bindgen_out. join ( format ! ( "{}_loader.js" , self . name) ) ;
686+ fs:: write ( & loader_src, shim. as_bytes ( ) )
684687 . await
685688 . context ( "error writing loader shim script" ) ?;
686- loader_f
687- . flush ( )
689+
690+ let hashed_loader_base = self . hashed_base ( loader_src. as_std_path ( ) ) . await ?;
691+ let hashed_loader_name = apply_data_target_path (
692+ format ! ( "{hashed_loader_base}_loader.js" ) ,
693+ & self . target_path ,
694+ ) ;
695+ let loader_shim_path = self . cfg . staging_dist . join ( & hashed_loader_name) ;
696+
697+ tracing:: debug!( "creating {}" , loader_shim_path. display( ) ) ;
698+ fs:: copy ( & loader_src, & loader_shim_path)
688699 . await
689- . context ( "error writing loader shim script" ) ?;
700+ . context ( "error copying loader shim script to stage dir " ) ?;
690701 }
691702
692703 // Check for any snippets, and copy them over.
@@ -819,16 +830,16 @@ impl RustApp {
819830 } )
820831 }
821832
822- /// create a cache busting hashed name for the wasm file , if enabled.
823- async fn hashed_wasm_base ( & self , wasm : & Path ) -> Result < String > {
833+ /// create a cache busting hashed name from the given artifact , if enabled.
834+ async fn hashed_base ( & self , path : & Path ) -> Result < String > {
824835 // Skip the hashed file name for workers as their file name must be named at runtime.
825836 // Therefore, workers use the Cargo binary name for file naming.
826837 if self . app_type == RustAppType :: Worker {
827838 return Ok ( self . name . clone ( ) ) ;
828839 }
829840
830841 Ok ( self
831- . hashed ( wasm )
842+ . hashed ( path )
832843 . await ?
833844 . map ( |hashed| format ! ( "{}-{hashed}" , self . name) )
834845 . unwrap_or_else ( || self . name . clone ( ) ) )
0 commit comments