@@ -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,21 @@ 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+
974+ if let Some ( server) = self . server . as_ref ( ) {
975+ let dir = server. build . crate_dir ( ) ;
976+ self . fill_scss_indirection ( & dir) ;
977+ }
978+
979+ for krate in self . all_watched_crates ( ) {
980+ self . fill_scss_indirection ( & krate) ;
981+ }
982+ }
983+
935984 /// Fill the filemap with files from the filesystem, using the given filter to determine which files to include.
936985 ///
937986 /// You can use the filter with something like a gitignore to only include files that are relevant to your project.
@@ -970,6 +1019,62 @@ impl AppServer {
9701019 }
9711020 }
9721021
1022+ /// Fill the scss indirection map with files from the filesystem, using the given filter to determine which files to include.
1023+ ///
1024+ /// Scss and Sass files will be scanned for @use statements and added to the indirection map
1025+ /// so their parent could be hot reloaded.
1026+ fn fill_scss_indirection ( & mut self , dir : & PathBuf ) {
1027+ for entry in walkdir:: WalkDir :: new ( dir) . into_iter ( ) . flatten ( ) {
1028+ if self
1029+ . workspace
1030+ . ignore
1031+ . matched ( entry. path ( ) , entry. file_type ( ) . is_dir ( ) )
1032+ . is_ignore ( )
1033+ {
1034+ continue ;
1035+ }
1036+
1037+ let path = entry. path ( ) ;
1038+ let ext = path. extension ( ) . and_then ( |s| s. to_str ( ) ) ;
1039+
1040+ if ext == Some ( "scss" ) || ext == Some ( "sass" ) {
1041+ if let Ok ( contents) = std:: fs:: read_to_string ( path) {
1042+ if let Some ( parent_dir) = path. parent ( ) {
1043+ let uses_at_rules = extract_use_paths ( parent_dir, & contents) ;
1044+ for at_rule_target in uses_at_rules {
1045+ self . scss_indirection_map
1046+ . insert ( at_rule_target, path. to_path_buf ( ) ) ;
1047+ }
1048+ } ;
1049+ }
1050+ }
1051+ }
1052+ }
1053+
1054+ /// Get the bundled assets referencing a given SCSS file by traversing the SCSS dependency tree upward.
1055+ /// Scss and Sass may either bundled or refenced with the `@use` scss at-rule.
1056+ async fn get_scss_bundled_assets ( & self , path : & Path ) -> Vec < PathBuf > {
1057+ let mut current = path;
1058+ let mut assets = Vec :: new ( ) ;
1059+
1060+ let mut visited = HashSet :: new ( ) ;
1061+
1062+ while let Some ( parent) = self . scss_indirection_map . get ( current) {
1063+ if !visited. insert ( parent) {
1064+ break ;
1065+ }
1066+
1067+ if let Some ( bundled_names) = self . client . hotreload_bundled_assets ( parent) . await {
1068+ for bundled_name in bundled_names {
1069+ assets. push ( PathBuf :: from ( "/assets/" ) . join ( bundled_name) ) ;
1070+ }
1071+ }
1072+ current = parent;
1073+ }
1074+
1075+ assets
1076+ }
1077+
9731078 /// Commit the changes to the filemap, overwriting the contents of the files
9741079 ///
9751080 /// Removes any cached templates and replaces the contents of the files with the most recent
@@ -1294,3 +1399,15 @@ fn is_wsl() -> bool {
12941399
12951400 false
12961401}
1402+
1403+ // Extract paths from @use statements in SCSS/SASS files
1404+ fn extract_use_paths ( parent_dir : & Path , scss : & str ) -> Vec < PathBuf > {
1405+ // Match @use "something/path.scss" or @use 'something/path' or @use "..." with or without semicolon/with options
1406+ // TODO: statically compile regex
1407+ let re = regex:: Regex :: new ( r#"(?:@use|@import)\s+['"]([^'"]+)['"]"# ) . unwrap ( ) ;
1408+ re. captures_iter ( scss)
1409+ . filter_map ( |cap| cap. get ( 1 ) . map ( |m| m. as_str ( ) . to_string ( ) ) )
1410+ . map ( PathBuf :: from)
1411+ . filter_map ( |path| parent_dir. join ( path) . canonicalize ( ) . ok ( ) )
1412+ . collect ( )
1413+ }
0 commit comments