Răsfoiți Sursa

fix: hotreloading files with multiple templates, asset hotreloading

Jonathan Kelley 1 an în urmă
părinte
comite
bca5335f31

+ 16 - 39
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",

+ 1 - 1
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"

+ 11 - 6
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;
 })()

+ 1 - 1
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<String>,

+ 2 - 2
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),

+ 1 - 1
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 {

+ 72 - 14
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);
     }
 }
 

+ 31 - 7
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::<Vec<_>>();
 
+    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(())
 }

+ 2 - 2
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<BuildResult> {
     // 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;

+ 2 - 0
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);
         }

+ 191 - 194
packages/hot-reload/src/file_watcher.rs

@@ -119,222 +119,219 @@ pub fn init<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
         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);
-
-        // convert the excluded paths to absolute paths
-        let excluded_paths = excluded_paths
-            .iter()
-            .map(|path| crate_dir.join(PathBuf::from(path)))
-            .collect::<Vec<_>>();
-
-        let channels = Arc::new(Mutex::new(Vec::new()));
-        let FileMapBuildResult {
-            map: file_map,
-            errors,
-        } = FileMap::<Ctx>::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 Ok(crate_dir) = PathBuf::from_str(root_path) else {
+        return;
+    };
+
+    // try to find the gitignore file
+    let gitignore_file_path = crate_dir.join(".gitignore");
+    let (gitignore, _) = ignore::gitignore::Gitignore::new(gitignore_file_path);
+
+    // convert the excluded paths to absolute paths
+    let excluded_paths = excluded_paths
+        .iter()
+        .map(|path| crate_dir.join(PathBuf::from(path)))
+        .collect::<Vec<_>>();
+
+    let channels = Arc::new(Mutex::new(Vec::new()));
+    let FileMapBuildResult {
+        map: file_map,
+        errors,
+    } = FileMap::<Ctx>::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 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());
         }
+    }
 
-        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;
-                            }
+    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();
+    // watch for changes
+    std::thread::spawn(move || {
+        let mut last_update_time = chrono::Local::now().timestamp();
 
-                    let (tx, rx) = std::sync::mpsc::channel();
+        let (tx, rx) = std::sync::mpsc::channel();
 
-                    let mut watcher =
-                        RecommendedWatcher::new(tx, notify::Config::default()).unwrap();
+        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:?}",
-                                );
-                            }
-                        }
+        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();
 
-                    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;
+                    }
 
-                                if shutdown {
-                                    *aborted.lock().unwrap() = true;
-                                }
+                    for channel in &mut *channels.lock().unwrap() {
+                        send_msg(HotReloadMsg::Shutdown, channel);
+                    }
 
-                                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
+            }
+        };
 
-                                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;
+            }
 
-                    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::<Vec<_>>();
-
-                                // 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 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::<Vec<_>>();
 
-                                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:?}"
-                                                );
-                                            }
-                                        }
-                                    }
+            // 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);
                                 }
                             }
-                            last_update_time = chrono::Local::now().timestamp_millis();
                         }
                     }
-                });
+
+                    Ok(UpdateResult::NeedsRebuild) => {
+                        drop(channels);
+                        if rebuild() {
+                            return;
+                        }
+                        break;
+                    }
+                    Err(err) => {
+                        if log {
+                            println!("hot reloading failed to update rsx:\n{err:?}");
+                        }
+                    }
+                }
             }
-            Err(error) => println!("failed to connect to hot reloading\n{error}"),
+
+            last_update_time = chrono::Local::now().timestamp_millis();
         }
-    }
+    });
 }
 
 fn send_msg(msg: HotReloadMsg, channel: &mut impl Write) -> bool {

+ 28 - 13
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<ChangedRsx> },
+}
+
+#[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<ChangedRsx>) -> 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<ChangedRsx>,
 ) -> 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<ChangedRsx>,
 ) -> 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<ChangedRsx>,
 ) -> 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<ChangedRsx>,
 ) -> 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<ChangedRsx>,
 ) -> 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

+ 299 - 126
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<Template>),
+
     NeedsRebuild,
 }
 
@@ -23,23 +34,27 @@ pub enum UpdateResult {
 pub struct FileMapBuildResult<Ctx: HotReloadingContext> {
     /// The FileMap that was built
     pub map: FileMap<Ctx>,
+
     /// Any errors that occurred while building the FileMap that were not fatal
     pub errors: Vec<io::Error>,
 }
 
 pub struct FileMap<Ctx: HotReloadingContext> {
-    pub map: HashMap<PathBuf, (String, Option<Template>)>,
+    pub map: HashMap<PathBuf, CachedSynFile>,
 
     in_workspace: HashMap<PathBuf, Option<PathBuf>>,
 
     phantom: std::marker::PhantomData<Ctx>,
 }
 
-struct CachedSynFile {
-    raw: String,
-    file: syn::File,
-    path: PathBuf,
-    template: Option<Template>,
+/// A cached file that has been parsed
+///
+/// We store the templates found in this file
+pub struct CachedSynFile {
+    pub raw: String,
+    pub path: PathBuf,
+    pub templates: HashMap<&'static str, Template>,
+    pub tracked_assets: HashSet<PathBuf>,
 }
 
 impl<Ctx: HotReloadingContext> FileMap<Ctx> {
@@ -53,146 +68,216 @@ impl<Ctx: HotReloadingContext> FileMap<Ctx> {
         path: PathBuf,
         mut filter: impl FnMut(&Path) -> bool,
     ) -> io::Result<FileMapBuildResult<Ctx>> {
-        struct FileMapSearchResult {
-            map: HashMap<PathBuf, (String, Option<Template>)>,
-            errors: Vec<io::Error>,
-        }
-        fn find_rs_files(
-            root: PathBuf,
-            filter: &mut impl FnMut(&Path) -> bool,
-        ) -> FileMapSearchResult {
-            let mut files = HashMap::new();
-            let mut errors = Vec::new();
-            if root.is_dir() {
-                let read_dir = match fs::read_dir(root) {
-                    Ok(read_dir) => read_dir,
-                    Err(err) => {
-                        errors.push(err);
-                        return FileMapSearchResult { map: files, errors };
-                    }
-                };
-                for entry in read_dir.flatten() {
-                    let path = entry.path();
-                    if !filter(&path) {
-                        let FileMapSearchResult {
-                            map,
-                            errors: child_errors,
-                        } = find_rs_files(path, filter);
-                        errors.extend(child_errors);
-                        files.extend(map);
-                    }
-                }
-            } else if root.extension().and_then(|s| s.to_str()) == Some("rs") {
-                if let Ok(mut file) = File::open(root.clone()) {
-                    let mut src = String::new();
-                    match file.read_to_string(&mut src) {
-                        Ok(_) => {
-                            files.insert(root, (src, None));
-                        }
-                        Err(err) => {
-                            errors.push(err);
-                        }
-                    }
-                }
-            }
-            FileMapSearchResult { map: files, errors }
-        }
-
         let FileMapSearchResult { map, errors } = find_rs_files(path, &mut filter);
-        let result = Self {
-            map,
-            in_workspace: HashMap::new(),
-            phantom: std::marker::PhantomData,
-        };
-        Ok(FileMapBuildResult {
-            map: result,
+
+        let file_map_build_result = FileMapBuildResult {
             errors,
-        })
+            map: Self {
+                map,
+                in_workspace: HashMap::new(),
+                phantom: std::marker::PhantomData,
+            },
+        };
+
+        Ok(file_map_build_result)
     }
 
     /// Try to update the rsx in a file
-    pub fn update_rsx(&mut self, file_path: &Path, crate_dir: &Path) -> io::Result<UpdateResult> {
+    pub fn update_rsx(
+        &mut self,
+        file_path: &Path,
+        crate_dir: &Path,
+    ) -> Result<UpdateResult, HotreloadError> {
         let mut file = File::open(file_path)?;
         let mut src = String::new();
         file.read_to_string(&mut src)?;
 
         // If we can't parse the contents we want to pass it off to the build system to tell the user that there's a syntax error
-        let Ok(syntax) = syn::parse_file(&src) else {
+        let syntax = syn::parse_file(&src).map_err(|_err| HotreloadError::Parse)?;
+
+        let in_workspace = self.child_in_workspace(crate_dir)?;
+
+        // Get the cached file if it exists, otherwise try to create it
+        let Some(old_cached) = self.map.get_mut(file_path) else {
+            // if this is a new file, rebuild the project
+            let FileMapBuildResult { map, mut errors } =
+                FileMap::<Ctx>::create(crate_dir.to_path_buf())?;
+
+            if let Some(err) = errors.pop() {
+                return Err(HotreloadError::Failure(err));
+            }
+
+            // merge the new map into the old map
+            self.map.extend(map.map);
+
             return Ok(UpdateResult::NeedsRebuild);
         };
 
-        let in_workspace = self.child_in_workspace(crate_dir)?;
+        // If the cached file is not a valid rsx file, rebuild the project, forcing errors
+        // TODO: in theory the error is simply in the RsxCallbody. We could attempt to parse it using partial expansion
+        // And collect out its errors instead of giving up to a full rebuild
+        let old = syn::parse_file(&*old_cached.raw).map_err(|_e| HotreloadError::Parse)?;
 
-        if let Some((old_src, template_slot)) = self.map.get_mut(file_path) {
-            if let Ok(old) = syn::parse_file(old_src) {
-                match find_rsx(&syntax, &old) {
-                    DiffResult::CodeChanged => {
-                        self.map.insert(file_path.to_path_buf(), (src, None));
-                    }
-                    DiffResult::RsxChanged(changed) => {
-                        let mut messages: Vec<Template> = Vec::new();
-                        for (old, new) in changed.into_iter() {
-                            let old_start = old.span().start();
-
-                            if let (Ok(old_call_body), Ok(new_call_body)) = (
-                                syn::parse2::<CallBody>(old.tokens),
-                                syn::parse2::<CallBody>(new),
-                            ) {
-                                // if the file!() macro is invoked in a workspace, the path is relative to the workspace root, otherwise it's relative to the crate root
-                                // we need to check if the file is in a workspace or not and strip the prefix accordingly
-                                let prefix = if let Some(workspace) = &in_workspace {
-                                    workspace
-                                } else {
-                                    crate_dir
-                                };
-                                if let Ok(file) = file_path.strip_prefix(prefix) {
-                                    let line = old_start.line;
-                                    let column = old_start.column + 1;
-                                    let location = file.display().to_string()
-                                        + ":"
-                                        + &line.to_string()
-                                        + ":"
-                                        + &column.to_string()
-                                        // the byte index doesn't matter, but dioxus needs it
-                                        + ":0";
-
-                                    if let Some(template) = new_call_body.update_template::<Ctx>(
-                                        Some(old_call_body),
-                                        Box::leak(location.into_boxed_str()),
-                                    ) {
-                                        // dioxus cannot handle empty templates
-                                        if template.roots.is_empty() {
-                                            return Ok(UpdateResult::NeedsRebuild);
-                                        } else {
-                                            // if the template is the same, don't send it
-                                            if let Some(old_template) = template_slot {
-                                                if old_template == &template {
-                                                    continue;
-                                                }
-                                            }
-                                            *template_slot = Some(template);
-                                            messages.push(template);
-                                        }
-                                    } else {
-                                        return Ok(UpdateResult::NeedsRebuild);
-                                    }
+        let instances = match find_rsx(&syntax, &old) {
+            // If the changes were just some rsx, we can just update the template
+            //
+            // However... if the changes involved code in the rsx itself, this should actually be a CodeChanged
+            DiffResult::RsxChanged {
+                rsx_calls: instances,
+            } => instances,
+
+            // If the changes were some code, we should insert the file into the map and rebuild
+            // todo: not sure we even need to put the cached file into the map, but whatever
+            DiffResult::CodeChanged => {
+                let cached_file = CachedSynFile {
+                    raw: src.clone(),
+                    path: file_path.to_path_buf(),
+                    templates: HashMap::new(),
+                    tracked_assets: HashSet::new(),
+                };
+
+                self.map.insert(file_path.to_path_buf(), cached_file);
+                return Ok(UpdateResult::NeedsRebuild);
+            }
+        };
+
+        println!("instances: {:?}", instances);
+
+        let mut messages: Vec<Template> = Vec::new();
+
+        for calls in instances.into_iter() {
+            let ChangedRsx { old, new } = calls;
+
+            let old_start = old.span().start();
+
+            let old_parsed = syn::parse2::<CallBody>(old.tokens);
+            let new_parsed = syn::parse2::<CallBody>(new);
+            let (Ok(old_call_body), Ok(new_call_body)) = (old_parsed, new_parsed) else {
+                continue;
+            };
+
+            // if the file!() macro is invoked in a workspace, the path is relative to the workspace root, otherwise it's relative to the crate root
+            // we need to check if the file is in a workspace or not and strip the prefix accordingly
+            let prefix = match in_workspace {
+                Some(ref workspace) => workspace,
+                _ => crate_dir,
+            };
+
+            let Ok(file) = file_path.strip_prefix(prefix) else {
+                continue;
+            };
+
+            // We leak the template since templates are a compiletime value
+            // This is not ideal, but also not a huge deal for hot reloading
+            // TODO: we could consider arena allocating the templates and dropping them when the connection is closed
+            let leaked_location = Box::leak(template_location(old_start, file).into_boxed_str());
+
+            // Retuns Some(template) if the template is hotreloadable
+            // dynamic changes are not hot reloadable and force a rebuild
+            let hotreloadable_template =
+                new_call_body.update_template::<Ctx>(Some(old_call_body), leaked_location);
+
+            // if the template is not hotreloadable, we need to do a full rebuild
+            let Some(template) = hotreloadable_template else {
+                return Ok(UpdateResult::NeedsRebuild);
+            };
+
+            // dioxus cannot handle empty templates...
+            // todo: I think it can? or we just skip them nowa
+            if template.roots.is_empty() {
+                continue;
+            }
+
+            // if the template is the same, don't send it
+            if let Some(old_template) = old_cached.templates.get(template.name) {
+                if old_template == &template {
+                    continue;
+                }
+            };
+
+            // update the cached file
+            old_cached.templates.insert(template.name, template.clone());
+
+            // Track any new assets
+            old_cached
+                .tracked_assets
+                .extend(Self::populate_assets(template));
+
+            messages.push(template);
+        }
+
+        Ok(UpdateResult::UpdatedRsx(messages))
+    }
+
+    fn populate_assets(template: Template) -> HashSet<PathBuf> {
+        fn collect_assetlike_attrs(node: &TemplateNode, asset_urls: &mut HashSet<PathBuf>) {
+            match node {
+                TemplateNode::Element {
+                    attrs, children, ..
+                } => {
+                    for attr in attrs.iter() {
+                        match attr {
+                            TemplateAttribute::Static { name, value, .. } => {
+                                if *name == "src" || *name == "href" {
+                                    asset_urls.insert(PathBuf::from(*value));
                                 }
                             }
+                            _ => {}
                         }
-                        return Ok(UpdateResult::UpdatedRsx(messages));
+                    }
+
+                    for child in children.iter() {
+                        collect_assetlike_attrs(child, asset_urls);
                     }
                 }
+                _ => {}
             }
+        }
+
+        let mut asset_urls = HashSet::new();
+
+        for node in template.roots {
+            collect_assetlike_attrs(node, &mut asset_urls);
+        }
+
+        println!("asset urls: {:?}", asset_urls);
+
+        asset_urls
+    }
+
+    /// add the template to an existing file in the filemap if it exists
+    /// create a new file if it doesn't exist
+    pub fn insert(&mut self, path: PathBuf, template: Template) {
+        let tracked_assets = Self::populate_assets(template);
+
+        if self.map.contains_key(&path) {
+            let entry = self.map.get_mut(&path).unwrap();
+            entry.tracked_assets.extend(tracked_assets);
+            entry.templates.insert(template.name, template);
         } else {
-            // if this is a new file, rebuild the project
-            let FileMapBuildResult { map, mut errors } = FileMap::create(crate_dir.to_path_buf())?;
-            if let Some(err) = errors.pop() {
-                return Err(err);
-            }
-            *self = map;
+            self.map.insert(
+                path.clone(),
+                CachedSynFile {
+                    raw: String::new(),
+                    path,
+                    tracked_assets,
+                    templates: HashMap::from([(template.name, template)]),
+                },
+            );
         }
+    }
+
+    pub fn tracked_assets(&self) -> HashSet<PathBuf> {
+        self.map
+            .values()
+            .flat_map(|file| file.tracked_assets.iter().cloned())
+            .collect()
+    }
 
-        Ok(UpdateResult::NeedsRebuild)
+    pub fn is_tracking_asset(&self, path: &PathBuf) -> Option<&CachedSynFile> {
+        self.map
+            .values()
+            .find(|file| file.tracked_assets.contains(path))
     }
 
     fn child_in_workspace(&mut self, crate_dir: &Path) -> io::Result<Option<PathBuf>> {
@@ -215,3 +300,91 @@ impl<Ctx: HotReloadingContext> FileMap<Ctx> {
         }
     }
 }
+
+fn template_location(old_start: proc_macro2::LineColumn, file: &Path) -> String {
+    let line = old_start.line;
+    let column = old_start.column + 1;
+    let location = file.display().to_string()
+                                + ":"
+                                + &line.to_string()
+                                + ":"
+                                + &column.to_string()
+                                // the byte index doesn't matter, but dioxus needs it
+                                + ":0";
+    location
+}
+
+struct FileMapSearchResult {
+    map: HashMap<PathBuf, CachedSynFile>,
+    errors: Vec<io::Error>,
+}
+
+// todo: we could just steal the mod logic from rustc itself
+fn find_rs_files(root: PathBuf, filter: &mut impl FnMut(&Path) -> bool) -> FileMapSearchResult {
+    let mut files = HashMap::new();
+    let mut errors = Vec::new();
+
+    if root.is_dir() {
+        let read_dir = match fs::read_dir(root) {
+            Ok(read_dir) => read_dir,
+            Err(err) => {
+                errors.push(err);
+                return FileMapSearchResult { map: files, errors };
+            }
+        };
+        for entry in read_dir.flatten() {
+            let path = entry.path();
+            if !filter(&path) {
+                let FileMapSearchResult {
+                    map,
+                    errors: child_errors,
+                } = find_rs_files(path, filter);
+                errors.extend(child_errors);
+                files.extend(map);
+            }
+        }
+    } else if root.extension().and_then(|s| s.to_str()) == Some("rs") {
+        if let Ok(mut file) = File::open(root.clone()) {
+            let mut src = String::new();
+            match file.read_to_string(&mut src) {
+                Ok(_) => {
+                    let cached_file = CachedSynFile {
+                        raw: src.clone(),
+                        path: root.clone(),
+                        templates: HashMap::new(),
+                        tracked_assets: HashSet::new(),
+                    };
+                    files.insert(root, cached_file);
+                }
+                Err(err) => {
+                    errors.push(err);
+                }
+            }
+        }
+    }
+
+    FileMapSearchResult { map: files, errors }
+}
+
+#[derive(Debug)]
+pub enum HotreloadError {
+    Failure(io::Error),
+    Parse,
+    NoPreviousBuild,
+}
+
+impl std::fmt::Display for HotreloadError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Self::Failure(err) => write!(f, "Failed to parse file: {}", err),
+            Self::Parse => write!(f, "Failed to parse file"),
+            Self::NoPreviousBuild => write!(f, "No previous build found"),
+        }
+    }
+}
+
+impl From<io::Error> for HotreloadError {
+    fn from(err: io::Error) -> Self {
+        HotreloadError::Failure(err)
+    }
+}

+ 1 - 0
packages/rsx/src/lib.rs

@@ -74,6 +74,7 @@ impl CallBody {
             roots: &self.roots,
             location: None,
         };
+
         renderer.update_template::<Ctx>(template, location)
     }
 

+ 12 - 20
packages/web/Cargo.toml

@@ -24,7 +24,11 @@ wasm-bindgen-futures = "0.4.29"
 tracing = { workspace = true }
 rustc-hash = { workspace = true }
 console_error_panic_hook = { version = "0.1.7", optional = true }
-futures-util = { workspace = true, features = ["std", "async-await", "async-await-macro"] }
+futures-util = { workspace = true, features = [
+    "std",
+    "async-await",
+    "async-await-macro",
+] }
 futures-channel = { workspace = true }
 serde_json = { version = "1.0" }
 serde = { version = "1.0" }
@@ -43,35 +47,23 @@ features = [
     "Text",
     "Window",
     "DataTransfer",
-    "console"
+    "console",
+    "NodeList",
 ]
 
 [features]
 default = ["panic_hook", "mounted", "file_engine", "hot_reload", "eval"]
 panic_hook = ["console_error_panic_hook"]
-hydrate = [
-    "web-sys/Comment",
-]
-mounted = [
-    "web-sys/Element",
-    "dioxus-html/mounted"
-]
+hydrate = ["web-sys/Comment"]
+mounted = ["web-sys/Element", "dioxus-html/mounted"]
 file_engine = [
     "web-sys/File",
     "web-sys/FileList",
     "web-sys/FileReader",
-    "async-trait"
-]
-hot_reload = [
-    "web-sys/MessageEvent",
-    "web-sys/WebSocket",
-    "web-sys/Location",
-]
-eval = [
-    "dioxus-html/eval",
-    "serde-wasm-bindgen",
-    "async-trait"
+    "async-trait",
 ]
+hot_reload = ["web-sys/MessageEvent", "web-sys/WebSocket", "web-sys/Location"]
+eval = ["dioxus-html/eval", "serde-wasm-bindgen", "async-trait"]
 
 [dev-dependencies]
 dioxus = { workspace = true }

+ 31 - 5
packages/web/src/hot_reload.rs

@@ -3,6 +3,7 @@
 use futures_channel::mpsc::UnboundedReceiver;
 
 use dioxus_core::Template;
+use web_sys::{console, Element};
 
 pub(crate) fn init() -> UnboundedReceiver<Template> {
     use wasm_bindgen::closure::Closure;
@@ -29,13 +30,38 @@ pub(crate) fn init() -> UnboundedReceiver<Template> {
 
     // change the rsx when new data is received
     let cl = Closure::wrap(Box::new(move |e: MessageEvent| {
+        console::log_1(&e.clone().into());
+
         if let Ok(text) = e.data().dyn_into::<js_sys::JsString>() {
             let string: String = text.into();
-            let val = serde_json::from_str::<serde_json::Value>(&string).unwrap();
-            // leak the value
-            let val: &'static serde_json::Value = Box::leak(Box::new(val));
-            let template: Template = Template::deserialize(val).unwrap();
-            tx.unbounded_send(template).unwrap();
+
+            if let Ok(val) = serde_json::from_str::<serde_json::Value>(&string) {
+                // leak the value
+                let val: &'static serde_json::Value = Box::leak(Box::new(val));
+                let template: Template = Template::deserialize(val).unwrap();
+                tx.unbounded_send(template).unwrap();
+            }
+        }
+
+        // it might be triggering a reload of assets
+        // invalidate all the stylesheets on the page
+        let links = web_sys::window()
+            .unwrap()
+            .document()
+            .unwrap()
+            .query_selector_all("link[rel=stylesheet]")
+            .unwrap();
+
+        console::log_1(&links.clone().into());
+
+        for x in 0..links.length() {
+            use wasm_bindgen::JsCast;
+            use web_sys::Element;
+            console::log_1(&x.clone().into());
+
+            let link: Element = links.get(x).unwrap().unchecked_into();
+            let href = link.get_attribute("href").unwrap();
+            link.set_attribute("href", &format!("{}?{}", href, js_sys::Math::random()));
         }
     }) as Box<dyn FnMut(MessageEvent)>);