From bca5335f3116bf696f4d9c7a729bbbd15db29fee Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 13 Mar 2024 02:13:37 -0700 Subject: [PATCH] fix: hotreloading files with multiple templates, asset hotreloading --- Cargo.lock | 55 +-- packages/cli/Cargo.toml | 2 +- packages/cli/src/assets/autoreload.js | 17 +- packages/cli/src/builder.rs | 2 +- packages/cli/src/cli/build.rs | 4 +- packages/cli/src/server/desktop/mod.rs | 2 +- packages/cli/src/server/mod.rs | 86 +++- packages/cli/src/server/web/hot_reload.rs | 38 +- packages/cli/src/server/web/mod.rs | 4 +- packages/core/src/virtual_dom.rs | 2 + packages/hot-reload/src/file_watcher.rs | 419 +++++++++-------- .../rsx/src/hot_reload/hot_reload_diff.rs | 41 +- .../src/hot_reload/hot_reloading_file_map.rs | 435 ++++++++++++------ packages/rsx/src/lib.rs | 1 + packages/web/Cargo.toml | 32 +- packages/web/src/hot_reload.rs | 36 +- 16 files changed, 723 insertions(+), 453 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f59bd8cfd..3d3267e18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1247,20 +1247,6 @@ dependencies = [ "serde", ] -[[package]] -name = "cargo_metadata" -version = "0.15.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a" -dependencies = [ - "camino", - "cargo-platform", - "semver", - "serde", - "serde_json", - "thiserror", -] - [[package]] name = "cargo_metadata" version = "0.17.0" @@ -1347,15 +1333,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "cfg-expr" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bbc13bf6290a6b202cc3efb36f7ec2b739a80634215630c8053a313edf6abef" -dependencies = [ - "smallvec", -] - [[package]] name = "cfg-expr" version = "0.15.7" @@ -2246,7 +2223,7 @@ dependencies = [ "pretty_assertions", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.51", ] [[package]] @@ -2284,7 +2261,7 @@ dependencies = [ "hyper-util", "ignore", "indicatif", - "interprocess-docfix", + "interprocess", "lazy_static", "log", "manganis-cli-support", @@ -2511,7 +2488,7 @@ dependencies = [ "dioxus-rsx", "execute", "ignore", - "interprocess-docfix", + "interprocess", "notify", "once_cell", "serde", @@ -5028,10 +5005,10 @@ dependencies = [ ] [[package]] -name = "interprocess-docfix" -version = "1.2.2" +name = "interprocess" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b84ee245c606aeb0841649a9288e3eae8c61b853a8cd5c0e14450e96d53d28f" +checksum = "81f2533f3be42fffe3b5e63b71aeca416c1c3bc33e4e27be018521e76b1f38fb" dependencies = [ "blocking", "cfg-if", @@ -5290,12 +5267,12 @@ dependencies = [ [[package]] name = "krates" -version = "0.12.6" +version = "0.16.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942c43a6cba1c201dfe81a943c89fa5c9140b34993e0c027f542c80b92e319a7" +checksum = "320d34cfe880f2c6243b4cfff8aab3e34eab6325d0a26729f23356418fbdc809" dependencies = [ - "cargo_metadata 0.15.4", - "cfg-expr 0.12.0", + "cargo_metadata 0.18.1", + "cfg-expr", "petgraph", "semver", ] @@ -6499,9 +6476,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "owo-colors" -version = "3.5.0" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" +checksum = "caff54706df99d2a78a5a4e3455ff45448d81ef1bb63c22cd14052ca0e993a3f" dependencies = [ "supports-color", ] @@ -8960,11 +8937,11 @@ checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "supports-color" -version = "1.3.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ba6faf2ca7ee42fdd458f4347ae0a9bd6bcc445ad7cb57ad82b383f18870d6f" +checksum = "d6398cde53adc3c4557306a96ce67b302968513830a77a95b2b17305d9719a89" dependencies = [ - "atty", + "is-terminal", "is_ci", ] @@ -9035,7 +9012,7 @@ version = "6.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2d580ff6a20c55dfb86be5f9c238f67835d0e81cbdea8bf5680e0897320331" dependencies = [ - "cfg-expr 0.15.7", + "cfg-expr", "heck 0.4.1", "pkg-config", "toml 0.8.10", diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index fc1668e50..fe4e0a8fe 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -24,7 +24,7 @@ serde_json = "1.0.79" toml = { workspace = true } fs_extra = "1.2.0" cargo_toml = "0.18.0" -futures-util = { workspace = true } +futures-util = { workspace = true, features = ["async-await-macro"] } notify = { version = "5.0.0-pre.16", features = ["serde"] } html_parser = { workspace = true } cargo_metadata = "0.18.1" diff --git a/packages/cli/src/assets/autoreload.js b/packages/cli/src/assets/autoreload.js index cc8fef166..3d7d40012 100644 --- a/packages/cli/src/assets/autoreload.js +++ b/packages/cli/src/assets/autoreload.js @@ -4,7 +4,8 @@ (function () { var protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; var url = protocol + '//' + window.location.host + '/_dioxus/ws'; - var poll_interval = 8080; + var poll_interval = 8080; + var reload_upon_connect = () => { window.setTimeout( () => { @@ -15,11 +16,15 @@ poll_interval); }; - var ws = new WebSocket(url); - ws.onmessage = (ev) => { - if (ev.data == "reload") { - window.location.reload(); - } + var ws = new WebSocket(url); + + ws.onmessage = (ev) => { + console.log("Received message: ", ev, ev.data); + + if (ev.data == "reload") { + window.location.reload(); + } }; + ws.onclose = reload_upon_connect; })() diff --git a/packages/cli/src/builder.rs b/packages/cli/src/builder.rs index f8b2b9ec0..cb91941e5 100644 --- a/packages/cli/src/builder.rs +++ b/packages/cli/src/builder.rs @@ -64,7 +64,7 @@ impl ExecWithRustFlagsSetter for subprocess::Exec { /// Build client (WASM). /// Note: `rust_flags` argument is only used for the fullstack platform. -pub fn build( +pub fn build_web( config: &CrateConfig, skip_assets: bool, rust_flags: Option, diff --git a/packages/cli/src/cli/build.rs b/packages/cli/src/cli/build.rs index 83149d8cf..d0139c322 100644 --- a/packages/cli/src/cli/build.rs +++ b/packages/cli/src/cli/build.rs @@ -57,7 +57,7 @@ impl Build { let build_result = match platform { Platform::Web => { // `rust_flags` are used by fullstack's client build. - crate::builder::build(&crate_config, self.build.skip_assets, rust_flags)? + crate::builder::build_web(&crate_config, self.build.skip_assets, rust_flags)? } Platform::Desktop => { // Since desktop platform doesn't use `rust_flags`, this @@ -80,7 +80,7 @@ impl Build { } None => web_config.features = Some(vec![web_feature]), }; - crate::builder::build( + crate::builder::build_web( &web_config, self.build.skip_assets, Some(client_rust_flags), diff --git a/packages/cli/src/server/desktop/mod.rs b/packages/cli/src/server/desktop/mod.rs index 10997582a..e6261eb43 100644 --- a/packages/cli/src/server/desktop/mod.rs +++ b/packages/cli/src/server/desktop/mod.rs @@ -138,7 +138,7 @@ async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<() .unwrap() .map .values() - .filter_map(|(_, template_slot)| *template_slot) + .flat_map(|v| v.templates.values().copied()) .collect() }; for template in templates { diff --git a/packages/cli/src/server/mod.rs b/packages/cli/src/server/mod.rs index f1d948478..21256bd0d 100644 --- a/packages/cli/src/server/mod.rs +++ b/packages/cli/src/server/mod.rs @@ -6,6 +6,7 @@ use dioxus_core::Template; use dioxus_hot_reload::HotReloadMsg; use dioxus_html::HtmlCtx; use dioxus_rsx::hot_reload::*; +use fs_extra::{dir::CopyOptions, file}; use notify::{RecommendedWatcher, Watcher}; use std::{ path::PathBuf, @@ -157,17 +158,9 @@ fn hotreload_files( for path in &event.paths { // for various assets that might be linked in, we just try to hotreloading them forcefully // That is, unless they appear in an include! macro, in which case we need to a full rebuild.... - - // if this is not a rust file, rebuild the whole project - let path_extension = path.extension().and_then(|p| p.to_str()); - - if path_extension != Some("rs") { - *needs_full_rebuild = true; - if path_extension == Some("rs~") { - *needs_full_rebuild = false; - } + let Some(ext) = path.extension().and_then(|v| v.to_str()) else { continue; - } + }; // Workaround for notify and vscode-like editor: // when edit & save a file in vscode, there will be two notifications, @@ -179,8 +172,67 @@ fn hotreload_files( } } + match ext { + // Attempt hot reload + "rs" => {} + + // Anything with a .file is also ignored + _ if path.file_stem().is_none() || ext.ends_with("~") => {} + + // Anything else is a maybe important file that needs to be rebuilt + _ => { + // If it happens to be a file in the asset directory, there's a chance we can hotreload it. + // Only css is currently supported for hotreload + if ext == "css" { + let asset_dir = config + .crate_dir + .join(&config.dioxus_config.application.asset_dir); + + if path.starts_with(&asset_dir) { + let local_path: PathBuf = path + .file_name() + .unwrap() + .to_str() + .unwrap() + .to_string() + .parse() + .unwrap(); + + println!( + "maybe tracking asset: {:?}, {:#?}", + local_path, + rsx_file_map.tracked_assets() + ); + + if let Some(f) = rsx_file_map.is_tracking_asset(&local_path) { + println!( + "Hot reloading asset - it's tracked by the rsx!: {:?}", + local_path + ); + + // copy the asset over tothe output directory + let output_dir = config.out_dir(); + fs_extra::copy_items( + &[path], + output_dir, + &CopyOptions::new().overwrite(true), + ) + .unwrap(); + + messages.push(HotReloadMsg::UpdateAsset(local_path)); + continue; + } + } + } + + *needs_full_rebuild = true; + } + }; + match rsx_file_map.update_rsx(path, &config.crate_dir) { Ok(UpdateResult::UpdatedRsx(msgs)) => { + println!("Updated: {:?}", msgs); + messages.extend( msgs.into_iter() .map(|msg| HotReloadMsg::UpdateTemplate(msg)), @@ -196,6 +248,8 @@ fn hotreload_files( } } + // If full rebuild, extend the file map with the new file map + // This will wipe away any previous cached changed templates if *needs_full_rebuild { // Reset the file map to the new state of the project let FileMapBuildResult { @@ -208,10 +262,14 @@ fn hotreload_files( } *rsx_file_map = new_file_map; - } else { - for msg in messages { - let _ = hot_reload.messages.send(msg); - } + + return; + } + + println!("Hot reloading: {:?}", messages); + + for msg in messages { + let _ = hot_reload.messages.send(msg); } } diff --git a/packages/cli/src/server/web/hot_reload.rs b/packages/cli/src/server/web/hot_reload.rs index 67d37535d..e72a7cc62 100644 --- a/packages/cli/src/server/web/hot_reload.rs +++ b/packages/cli/src/server/web/hot_reload.rs @@ -8,6 +8,7 @@ use axum::{ Extension, }; use dioxus_hot_reload::HotReloadMsg; +use futures_util::{pin_mut, FutureExt}; pub async fn hot_reload_handler( ws: WebSocketUpgrade, @@ -34,9 +35,11 @@ async fn hotreload_loop(mut socket: WebSocket, state: HotReloadState) -> anyhow: .unwrap() .map .values() - .filter_map(|(_, template_slot)| *template_slot) + .flat_map(|v| v.templates.values().copied()) .collect::>(); + println!("previously changed: {:?}", templates); + for template in templates { socket .send(Message::Text(serde_json::to_string(&template).unwrap())) @@ -46,16 +49,37 @@ async fn hotreload_loop(mut socket: WebSocket, state: HotReloadState) -> anyhow: let mut rx = state.messages.subscribe(); loop { - if let Ok(msg) = rx.recv().await { - let msg = match msg { + let msg = { + // Poll both the receiver and the socket + // + // This shuts us down if the connection is closed. + let mut _socket = socket.recv().fuse(); + let mut _rx = rx.recv().fuse(); + + pin_mut!(_socket, _rx); + + let msg = futures_util::select! { + msg = _rx => msg, + _ = _socket => break, + }; + + let Ok(msg) = msg else { break }; + + println!("msg: {:?}", msg); + + match msg { HotReloadMsg::UpdateTemplate(template) => { Message::Text(serde_json::to_string(&template).unwrap()) } - HotReloadMsg::UpdateAsset(_) => todo!(), + HotReloadMsg::UpdateAsset(asset) => { + Message::Text(format!("asset: {}", asset.display())) + } HotReloadMsg::Shutdown => todo!(), - }; + } + }; - socket.send(msg).await?; - } + socket.send(msg).await?; } + + Ok(()) } diff --git a/packages/cli/src/server/web/mod.rs b/packages/cli/src/server/web/mod.rs index e20447516..01ecddf8b 100644 --- a/packages/cli/src/server/web/mod.rs +++ b/packages/cli/src/server/web/mod.rs @@ -52,7 +52,7 @@ pub async fn serve( // Since web platform doesn't use `rust_flags`, this argument is explicitly // set to `None`. - let first_build_result = crate::builder::build(&config, skip_assets, None)?; + let first_build_result = crate::builder::build_web(&config, skip_assets, None)?; // generate dev-index page Serve::regen_dev_page(&config, first_build_result.assets.as_ref())?; @@ -173,7 +173,7 @@ fn build( ) -> Result { // Since web platform doesn't use `rust_flags`, this argument is explicitly // set to `None`. - let result = std::panic::catch_unwind(|| builder::build(config, skip_assets, None)) + let result = std::panic::catch_unwind(|| builder::build_web(config, skip_assets, None)) .map_err(|e| anyhow::anyhow!("Build failed: {e:?}"))?; // change the websocket reload state to true; diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 68046108c..0d5f84e11 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -534,6 +534,7 @@ impl VirtualDom { #[instrument(skip(self), level = "trace", name = "VirtualDom::replace_template")] pub fn replace_template(&mut self, template: Template) { self.register_template_first_byte_index(template); + // iterating a slab is very inefficient, but this is a rare operation that will only happen during development so it's fine let mut dirty = Vec::new(); for (id, scope) in self.scopes.iter() { @@ -545,6 +546,7 @@ impl VirtualDom { } } } + for dirty in dirty { self.mark_dirty(dirty); } diff --git a/packages/hot-reload/src/file_watcher.rs b/packages/hot-reload/src/file_watcher.rs index 5930684ea..aae482a69 100644 --- a/packages/hot-reload/src/file_watcher.rs +++ b/packages/hot-reload/src/file_watcher.rs @@ -119,222 +119,219 @@ pub fn init(cfg: Config) { phantom: _, } = cfg; - if let Ok(crate_dir) = PathBuf::from_str(root_path) { - // try to find the gitignore file - let gitignore_file_path = crate_dir.join(".gitignore"); - let (gitignore, _) = ignore::gitignore::Gitignore::new(gitignore_file_path); + let Ok(crate_dir) = PathBuf::from_str(root_path) else { + return; + }; - // convert the excluded paths to absolute paths - let excluded_paths = excluded_paths - .iter() - .map(|path| crate_dir.join(PathBuf::from(path))) - .collect::>(); + // try to find the gitignore file + let gitignore_file_path = crate_dir.join(".gitignore"); + let (gitignore, _) = ignore::gitignore::Gitignore::new(gitignore_file_path); - let channels = Arc::new(Mutex::new(Vec::new())); - let FileMapBuildResult { - map: file_map, - errors, - } = FileMap::::create_with_filter(crate_dir.clone(), |path| { - // skip excluded paths - excluded_paths.iter().any(|p| path.starts_with(p)) || - // respect .gitignore - gitignore - .matched_path_or_any_parents(path, path.is_dir()) - .is_ignore() - }) - .unwrap(); - for err in errors { - if log { - println!("hot reloading failed to initialize:\n{err:?}"); - } - } - let file_map = Arc::new(Mutex::new(file_map)); + // convert the excluded paths to absolute paths + let excluded_paths = excluded_paths + .iter() + .map(|path| crate_dir.join(PathBuf::from(path))) + .collect::>(); - let target_dir = crate_dir.join("target"); - let hot_reload_socket_path = target_dir.join("dioxusin"); - - #[cfg(unix)] - { - // On unix, if you force quit the application, it can leave the file socket open - // This will cause the local socket listener to fail to open - // We check if the file socket is already open from an old session and then delete it - if hot_reload_socket_path.exists() { - let _ = std::fs::remove_file(hot_reload_socket_path.clone()); - } - } - - match LocalSocketListener::bind(hot_reload_socket_path) { - Ok(local_socket_stream) => { - let aborted = Arc::new(Mutex::new(false)); - - // listen for connections - std::thread::spawn({ - let file_map = file_map.clone(); - let channels = channels.clone(); - let aborted = aborted.clone(); - let _ = local_socket_stream.set_nonblocking(true); - move || { - loop { - if let Ok(mut connection) = local_socket_stream.accept() { - // send any templates than have changed before the socket connected - let templates: Vec<_> = { - file_map - .lock() - .unwrap() - .map - .values() - .filter_map(|(_, template_slot)| *template_slot) - .collect() - }; - for template in templates { - if !send_msg( - HotReloadMsg::UpdateTemplate(template), - &mut connection, - ) { - continue; - } - } - channels.lock().unwrap().push(connection); - if log { - println!("Connected to hot reloading 🚀"); - } - } - if *aborted.lock().unwrap() { - break; - } - } - } - }); - - // watch for changes - std::thread::spawn(move || { - let mut last_update_time = chrono::Local::now().timestamp(); - - let (tx, rx) = std::sync::mpsc::channel(); - - let mut watcher = - RecommendedWatcher::new(tx, notify::Config::default()).unwrap(); - - for path in listening_paths { - let full_path = crate_dir.join(path); - if let Err(err) = watcher.watch(&full_path, RecursiveMode::Recursive) { - if log { - println!( - "hot reloading failed to start watching {full_path:?}:\n{err:?}", - ); - } - } - } - - let mut rebuild = { - let aborted = aborted.clone(); - let channels = channels.clone(); - move || { - if let Some(rebuild_callback) = &mut rebuild_with { - if log { - println!("Rebuilding the application..."); - } - let shutdown = rebuild_callback(); - - if shutdown { - *aborted.lock().unwrap() = true; - } - - for channel in &mut *channels.lock().unwrap() { - send_msg(HotReloadMsg::Shutdown, channel); - } - - return shutdown; - } else if log { - println!( - "Rebuild needed... shutting down hot reloading.\nManually rebuild the application to view further changes." - ); - } - true - } - }; - - for evt in rx { - if chrono::Local::now().timestamp_millis() >= last_update_time { - if let Ok(evt) = evt { - let real_paths = evt - .paths - .iter() - .filter(|path| { - // skip non rust files - matches!( - path.extension().and_then(|p| p.to_str()), - Some("rs" | "toml" | "css" | "html" | "js") - ) && - // skip excluded paths - !excluded_paths.iter().any(|p| path.starts_with(p)) && - // respect .gitignore - !gitignore - .matched_path_or_any_parents(path, false) - .is_ignore() - }) - .collect::>(); - - // Give time for the change to take effect before reading the file - if !real_paths.is_empty() { - std::thread::sleep(std::time::Duration::from_millis(10)); - } - - let mut channels = channels.lock().unwrap(); - for path in real_paths { - // if this file type cannot be hot reloaded, rebuild the application - if path.extension().and_then(|p| p.to_str()) != Some("rs") - && rebuild() - { - return; - } - // find changes to the rsx in the file - match file_map - .lock() - .unwrap() - .update_rsx(path, crate_dir.as_path()) - { - Ok(UpdateResult::UpdatedRsx(msgs)) => { - for msg in msgs { - let mut i = 0; - while i < channels.len() { - let channel = &mut channels[i]; - if send_msg( - HotReloadMsg::UpdateTemplate(msg), - channel, - ) { - i += 1; - } else { - channels.remove(i); - } - } - } - } - Ok(UpdateResult::NeedsRebuild) => { - drop(channels); - if rebuild() { - return; - } - break; - } - Err(err) => { - if log { - println!( - "hot reloading failed to update rsx:\n{err:?}" - ); - } - } - } - } - } - last_update_time = chrono::Local::now().timestamp_millis(); - } - } - }); - } - Err(error) => println!("failed to connect to hot reloading\n{error}"), + let channels = Arc::new(Mutex::new(Vec::new())); + let FileMapBuildResult { + map: file_map, + errors, + } = FileMap::::create_with_filter(crate_dir.clone(), |path| { + // skip excluded paths + excluded_paths.iter().any(|p| path.starts_with(p)) || + // respect .gitignore + gitignore + .matched_path_or_any_parents(path, path.is_dir()) + .is_ignore() + }) + .unwrap(); + for err in errors { + if log { + println!("hot reloading failed to initialize:\n{err:?}"); } } + let file_map = Arc::new(Mutex::new(file_map)); + + let target_dir = crate_dir.join("target"); + let hot_reload_socket_path = target_dir.join("dioxusin"); + + #[cfg(unix)] + { + // On unix, if you force quit the application, it can leave the file socket open + // This will cause the local socket listener to fail to open + // We check if the file socket is already open from an old session and then delete it + if hot_reload_socket_path.exists() { + let _ = std::fs::remove_file(hot_reload_socket_path.clone()); + } + } + + let local_socket_stream = match LocalSocketListener::bind(hot_reload_socket_path) { + Ok(local_socket_stream) => local_socket_stream, + Err(err) => { + println!("failed to connect to hot reloading\n{err}"); + return; + } + }; + + let aborted = Arc::new(Mutex::new(false)); + + // listen for connections + std::thread::spawn({ + let file_map = file_map.clone(); + let channels = channels.clone(); + let aborted = aborted.clone(); + let _ = local_socket_stream.set_nonblocking(true); + move || { + loop { + if let Ok(mut connection) = local_socket_stream.accept() { + // send any templates than have changed before the socket connected + let templates: Vec<_> = { + file_map + .lock() + .unwrap() + .map + .values() + .flat_map(|v| v.templates.values().copied()) + .collect() + }; + + for template in templates { + if !send_msg(HotReloadMsg::UpdateTemplate(template), &mut connection) { + continue; + } + } + channels.lock().unwrap().push(connection); + if log { + println!("Connected to hot reloading 🚀"); + } + } + if *aborted.lock().unwrap() { + break; + } + } + } + }); + + // watch for changes + std::thread::spawn(move || { + let mut last_update_time = chrono::Local::now().timestamp(); + + let (tx, rx) = std::sync::mpsc::channel(); + + let mut watcher = RecommendedWatcher::new(tx, notify::Config::default()).unwrap(); + + for path in listening_paths { + let full_path = crate_dir.join(path); + if let Err(err) = watcher.watch(&full_path, RecursiveMode::Recursive) { + if log { + println!("hot reloading failed to start watching {full_path:?}:\n{err:?}",); + } + } + } + + let mut rebuild = { + let aborted = aborted.clone(); + let channels = channels.clone(); + move || { + if let Some(rebuild_callback) = &mut rebuild_with { + if log { + println!("Rebuilding the application..."); + } + let shutdown = rebuild_callback(); + + if shutdown { + *aborted.lock().unwrap() = true; + } + + for channel in &mut *channels.lock().unwrap() { + send_msg(HotReloadMsg::Shutdown, channel); + } + + return shutdown; + } else if log { + println!("Rebuild needed... shutting down hot reloading.\nManually rebuild the application to view further changes."); + } + true + } + }; + + for evt in rx { + if chrono::Local::now().timestamp_millis() < last_update_time { + continue; + } + + let Ok(evt) = evt else { + continue; + }; + + let real_paths = evt + .paths + .iter() + .filter(|path| { + // skip non rust files + matches!( + path.extension().and_then(|p| p.to_str()), + Some("rs" | "toml" | "css" | "html" | "js") + ) && + // skip excluded paths + !excluded_paths.iter().any(|p| path.starts_with(p)) && + // respect .gitignore + !gitignore + .matched_path_or_any_parents(path, false) + .is_ignore() + }) + .collect::>(); + + // Give time for the change to take effect before reading the file + if !real_paths.is_empty() { + std::thread::sleep(std::time::Duration::from_millis(10)); + } + + let mut channels = channels.lock().unwrap(); + for path in real_paths { + // if this file type cannot be hot reloaded, rebuild the application + if path.extension().and_then(|p| p.to_str()) != Some("rs") && rebuild() { + return; + } + // find changes to the rsx in the file + match file_map + .lock() + .unwrap() + .update_rsx(path, crate_dir.as_path()) + { + Ok(UpdateResult::UpdatedRsx(msgs)) => { + for msg in msgs { + let mut i = 0; + while i < channels.len() { + let channel = &mut channels[i]; + if send_msg(HotReloadMsg::UpdateTemplate(msg), channel) { + i += 1; + } else { + channels.remove(i); + } + } + } + } + + Ok(UpdateResult::NeedsRebuild) => { + drop(channels); + if rebuild() { + return; + } + break; + } + Err(err) => { + if log { + println!("hot reloading failed to update rsx:\n{err:?}"); + } + } + } + } + + last_update_time = chrono::Local::now().timestamp_millis(); + } + }); } fn send_msg(msg: HotReloadMsg, channel: &mut impl Write) -> bool { diff --git a/packages/rsx/src/hot_reload/hot_reload_diff.rs b/packages/rsx/src/hot_reload/hot_reload_diff.rs index 3a1f0caaf..7d1a8ebb7 100644 --- a/packages/rsx/src/hot_reload/hot_reload_diff.rs +++ b/packages/rsx/src/hot_reload/hot_reload_diff.rs @@ -3,8 +3,22 @@ use quote::ToTokens; use syn::{File, Macro}; pub enum DiffResult { + /// Non-rsx was changed in the file CodeChanged, - RsxChanged(Vec<(Macro, TokenStream)>), + + /// Rsx was changed in the file + /// + /// Contains a list of macro invocations that were changed + RsxChanged { rsx_calls: Vec }, +} + +#[derive(Debug)] +pub struct ChangedRsx { + /// The macro that was changed + pub old: Macro, + + /// The new tokens for the macro + pub new: TokenStream, } /// Find any rsx calls in the given file and return a list of all the rsx calls that have changed. @@ -24,6 +38,7 @@ pub fn find_rsx(new: &File, old: &File) -> DiffResult { ); return DiffResult::CodeChanged; } + for (new, old) in new.items.iter().zip(old.items.iter()) { if find_rsx_item(new, old, &mut rsx_calls) { tracing::trace!( @@ -34,15 +49,12 @@ pub fn find_rsx(new: &File, old: &File) -> DiffResult { return DiffResult::CodeChanged; } } + tracing::trace!("found hot reload-able changes {:#?}", rsx_calls); - DiffResult::RsxChanged(rsx_calls) + DiffResult::RsxChanged { rsx_calls } } -fn find_rsx_item( - new: &syn::Item, - old: &syn::Item, - rsx_calls: &mut Vec<(Macro, TokenStream)>, -) -> bool { +fn find_rsx_item(new: &syn::Item, old: &syn::Item, rsx_calls: &mut Vec) -> bool { match (new, old) { (syn::Item::Const(new_item), syn::Item::Const(old_item)) => { find_rsx_expr(&new_item.expr, &old_item.expr, rsx_calls) @@ -190,7 +202,7 @@ fn find_rsx_item( fn find_rsx_trait( new_item: &syn::ItemTrait, old_item: &syn::ItemTrait, - rsx_calls: &mut Vec<(Macro, TokenStream)>, + rsx_calls: &mut Vec, ) -> bool { if new_item.items.len() != old_item.items.len() { return true; @@ -243,7 +255,7 @@ fn find_rsx_trait( fn find_rsx_block( new_block: &syn::Block, old_block: &syn::Block, - rsx_calls: &mut Vec<(Macro, TokenStream)>, + rsx_calls: &mut Vec, ) -> bool { if new_block.stmts.len() != old_block.stmts.len() { return true; @@ -259,7 +271,7 @@ fn find_rsx_block( fn find_rsx_stmt( new_stmt: &syn::Stmt, old_stmt: &syn::Stmt, - rsx_calls: &mut Vec<(Macro, TokenStream)>, + rsx_calls: &mut Vec, ) -> bool { match (new_stmt, old_stmt) { (syn::Stmt::Local(new_local), syn::Stmt::Local(old_local)) => { @@ -293,7 +305,7 @@ fn find_rsx_stmt( fn find_rsx_expr( new_expr: &syn::Expr, old_expr: &syn::Expr, - rsx_calls: &mut Vec<(Macro, TokenStream)>, + rsx_calls: &mut Vec, ) -> bool { match (new_expr, old_expr) { (syn::Expr::Array(new_expr), syn::Expr::Array(old_expr)) => { @@ -631,7 +643,7 @@ fn find_rsx_expr( fn find_rsx_macro( new_mac: &syn::Macro, old_mac: &syn::Macro, - rsx_calls: &mut Vec<(Macro, TokenStream)>, + rsx_calls: &mut Vec, ) -> bool { if matches!( new_mac @@ -648,7 +660,10 @@ fn find_rsx_macro( .as_deref(), Some("rsx" | "render") ) { - rsx_calls.push((old_mac.clone(), new_mac.tokens.clone())); + rsx_calls.push(ChangedRsx { + old: old_mac.clone(), + new: new_mac.tokens.clone(), + }); false } else { new_mac != old_mac diff --git a/packages/rsx/src/hot_reload/hot_reloading_file_map.rs b/packages/rsx/src/hot_reload/hot_reloading_file_map.rs index e958c0256..37ef95279 100644 --- a/packages/rsx/src/hot_reload/hot_reloading_file_map.rs +++ b/packages/rsx/src/hot_reload/hot_reloading_file_map.rs @@ -1,21 +1,32 @@ use crate::{CallBody, HotReloadingContext}; -use dioxus_core::Template; +use dioxus_core::{ + prelude::{TemplateAttribute, TemplateNode}, + Template, +}; use krates::cm::MetadataCommand; use krates::Cmd; pub use proc_macro2::TokenStream; pub use std::collections::HashMap; -use std::path::PathBuf; pub use std::sync::Mutex; pub use std::time::SystemTime; +use std::{ + collections::HashSet, + hash::Hash, + path::{Display, PathBuf}, +}; pub use std::{fs, io, path::Path}; pub use std::{fs::File, io::Read}; pub use syn::__private::ToTokens; use syn::spanned::Spanned; -use super::hot_reload_diff::{find_rsx, DiffResult}; +use super::{ + hot_reload_diff::{find_rsx, DiffResult}, + ChangedRsx, +}; pub enum UpdateResult { UpdatedRsx(Vec