@@ -24,7 +24,7 @@ use notify::{
2424use std:: {
2525 collections:: { HashMap , HashSet } ,
2626 net:: { IpAddr , TcpListener } ,
27- path:: PathBuf ,
27+ path:: { Path , PathBuf } ,
2828 sync:: Arc ,
2929 time:: Duration ,
3030} ;
@@ -54,6 +54,7 @@ pub(crate) struct AppServer {
5454 // Tracked state related to open builds and hot reloading
5555 pub ( crate ) applied_client_hot_reload_message : HotReloadMsg ,
5656 pub ( crate ) file_map : HashMap < PathBuf , CachedFile > ,
57+ pub ( crate ) scss_indirection_map : HashMap < PathBuf , PathBuf > ,
5758
5859 // Resolved args related to how we go about processing the rebuilds and logging
5960 pub ( crate ) use_hotpatch_engine : bool ,
@@ -209,6 +210,7 @@ impl AppServer {
209210 tw_watcher,
210211 server_args,
211212 client_args,
213+ scss_indirection_map : Default :: default ( ) ,
212214 } ;
213215
214216 // Only register the hot-reload stuff if we're watching the filesystem
@@ -223,6 +225,7 @@ impl AppServer {
223225 // really, we should be using depinfo to get the files that are actually used, but the depinfo file might not be around yet
224226 // todo(jon): see if we can just guess the depinfo file before it generates. might be stale but at least it catches most of the files
225227 runner. load_rsx_filemap ( ) ;
228+ runner. load_scss_filemap ( ) ;
226229 }
227230
228231 Ok ( runner)
@@ -385,6 +388,37 @@ impl AppServer {
385388 continue ;
386389 }
387390
391+ // Scss files may be referenced with `@use` at-rules by an asset we need to reload the parent
392+ if ext == "scss" || ext == "sass" {
393+ let Ok ( new_content) = std:: fs:: read_to_string ( path) else {
394+ tracing:: debug!(
395+ "Failed to read SCSS/SASS file while hotreloading: {:?}" ,
396+ path
397+ ) ;
398+ continue ;
399+ } ;
400+
401+ let Some ( parent_dir) = path. parent ( ) else {
402+ tracing:: debug!(
403+ "Failed to get parent directory of SCSS/SASS file while hotreloading: {:?}" ,
404+ path
405+ ) ;
406+ continue ;
407+ } ;
408+
409+ let uses_at_rules = extract_use_paths ( parent_dir, & new_content) ;
410+
411+ // Aggressively insert all at-rule targets into the map,
412+ // we prevent cycles when resolving the at-rule targets in `get_scss_bundled_assets`
413+ for at_rule_target in uses_at_rules {
414+ self . scss_indirection_map
415+ . insert ( at_rule_target, path. clone ( ) ) ;
416+ }
417+
418+ let scss_bundled_assets = self . get_scss_bundled_assets ( path) . await ;
419+ assets. extend ( scss_bundled_assets) ;
420+ }
421+
388422 // If it's a rust file, we want to hotreload it using the filemap
389423 if ext == "rs" {
390424 // And grabout the contents
@@ -932,6 +966,23 @@ impl AppServer {
932966 }
933967 }
934968
969+ /// Load the scss indirection map.
970+ fn load_scss_filemap ( & mut self ) {
971+ let dir = self . client . build . crate_dir ( ) ;
972+ self . fill_scss_indirection ( & dir) ;
973+ self . fill_scss_indirection ( & dir) ;
974+
975+ if let Some ( server) = self . server . as_ref ( ) {
976+ let dir = server. build . crate_dir ( ) ;
977+ self . fill_scss_indirection ( & dir) ;
978+ self . fill_scss_indirection ( & dir) ;
979+ }
980+
981+ for krate in self . all_watched_crates ( ) {
982+ self . fill_scss_indirection ( & krate) ;
983+ }
984+ }
985+
935986 /// Fill the filemap with files from the filesystem, using the given filter to determine which files to include.
936987 ///
937988 /// You can use the filter with something like a gitignore to only include files that are relevant to your project.
@@ -970,6 +1021,62 @@ impl AppServer {
9701021 }
9711022 }
9721023
1024+ /// Fill the scss indirection map with files from the filesystem, using the given filter to determine which files to include.
1025+ ///
1026+ /// Scss and Sass files will be scanned for @use statements and added to the indirection map
1027+ /// so their parent could be hot reloaded.
1028+ fn fill_scss_indirection ( & mut self , dir : & PathBuf ) {
1029+ for entry in walkdir:: WalkDir :: new ( dir) . into_iter ( ) . flatten ( ) {
1030+ if self
1031+ . workspace
1032+ . ignore
1033+ . matched ( entry. path ( ) , entry. file_type ( ) . is_dir ( ) )
1034+ . is_ignore ( )
1035+ {
1036+ continue ;
1037+ }
1038+
1039+ let path = entry. path ( ) ;
1040+ let ext = path. extension ( ) . and_then ( |s| s. to_str ( ) ) ;
1041+
1042+ if ext == Some ( "scss" ) || ext == Some ( "sass" ) {
1043+ if let Ok ( contents) = std:: fs:: read_to_string ( path) {
1044+ if let Some ( parent_dir) = path. parent ( ) {
1045+ let uses_at_rules = extract_use_paths ( parent_dir, & contents) ;
1046+ for at_rule_target in uses_at_rules {
1047+ self . scss_indirection_map
1048+ . insert ( at_rule_target, path. to_path_buf ( ) ) ;
1049+ }
1050+ } ;
1051+ }
1052+ }
1053+ }
1054+ }
1055+
1056+ /// Get the bundled assets referencing a given SCSS file by traversing the SCSS dependency tree upward.
1057+ /// Scss and Sass may either bundled or refenced with the `@use` scss at-rule.
1058+ async fn get_scss_bundled_assets ( & self , path : & Path ) -> Vec < PathBuf > {
1059+ let mut current = path;
1060+ let mut assets = Vec :: new ( ) ;
1061+
1062+ let mut visited = HashSet :: new ( ) ;
1063+
1064+ while let Some ( parent) = self . scss_indirection_map . get ( current) {
1065+ if !visited. insert ( parent) {
1066+ break ;
1067+ }
1068+
1069+ if let Some ( bundled_names) = self . client . hotreload_bundled_assets ( parent) . await {
1070+ for bundled_name in bundled_names {
1071+ assets. push ( PathBuf :: from ( "/assets/" ) . join ( bundled_name) ) ;
1072+ }
1073+ }
1074+ current = parent;
1075+ }
1076+
1077+ assets
1078+ }
1079+
9731080 /// Commit the changes to the filemap, overwriting the contents of the files
9741081 ///
9751082 /// Removes any cached templates and replaces the contents of the files with the most recent
@@ -1294,3 +1401,15 @@ fn is_wsl() -> bool {
12941401
12951402 false
12961403}
1404+
1405+ // Extract paths from @use statements in SCSS/SASS files
1406+ fn extract_use_paths ( parent_dir : & Path , scss : & str ) -> Vec < PathBuf > {
1407+ // Match @use "something/path.scss" or @use 'something/path' or @use "..." with or without semicolon/with options
1408+ // TODO: statically compile regex
1409+ let re = regex:: Regex :: new ( r#"@use\s+['"]([^'"]+)['"]"# ) . unwrap ( ) ;
1410+ re. captures_iter ( scss)
1411+ . filter_map ( |cap| cap. get ( 1 ) . map ( |m| m. as_str ( ) . to_string ( ) ) )
1412+ . map ( PathBuf :: from)
1413+ . filter_map ( |path| parent_dir. join ( path) . canonicalize ( ) . ok ( ) )
1414+ . collect ( )
1415+ }
0 commit comments