fix: hotreloading files with multiple templates, asset hotreloading

This commit is contained in:
Jonathan Kelley committed 2024-03-13 02:13:37 -07:00
1 parent 05476cad76
commit bca5335f31
16 files changed
+532 -262

No files matched your search

Generated
+16 -39
View File
@@ -1247,20 +1247,6 @@ dependencies = [
"serde", "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]] [[package]]
name = "cargo_metadata" name = "cargo_metadata"
version = "0.17.0" version = "0.17.0"
@@ -1347,15 +1333,6 @@ dependencies = [
"uuid", "uuid",
] ]
[[package]]
name = "cfg-expr"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bbc13bf6290a6b202cc3efb36f7ec2b739a80634215630c8053a313edf6abef"
dependencies = [
"smallvec",
]
[[package]] [[package]]
name = "cfg-expr" name = "cfg-expr"
version = "0.15.7" version = "0.15.7"
@@ -2246,7 +2223,7 @@ dependencies = [
"pretty_assertions", "pretty_assertions",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 1.0.109", "syn 2.0.51",
] ]
[[package]] [[package]]
@@ -2284,7 +2261,7 @@ dependencies = [
"hyper-util", "hyper-util",
"ignore", "ignore",
"indicatif", "indicatif",
"interprocess-docfix", "interprocess",
"lazy_static", "lazy_static",
"log", "log",
"manganis-cli-support", "manganis-cli-support",
@@ -2511,7 +2488,7 @@ dependencies = [
"dioxus-rsx", "dioxus-rsx",
"execute", "execute",
"ignore", "ignore",
"interprocess-docfix", "interprocess",
"notify", "notify",
"once_cell", "once_cell",
"serde", "serde",
@@ -5028,10 +5005,10 @@ dependencies = [
] ]
[[package]] [[package]]
name = "interprocess-docfix" name = "interprocess"
version = "1.2.2" version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b84ee245c606aeb0841649a9288e3eae8c61b853a8cd5c0e14450e96d53d28f" checksum = "81f2533f3be42fffe3b5e63b71aeca416c1c3bc33e4e27be018521e76b1f38fb"
dependencies = [ dependencies = [
"blocking", "blocking",
"cfg-if", "cfg-if",
@@ -5290,12 +5267,12 @@ dependencies = [
[[package]] [[package]]
name = "krates" name = "krates"
version = "0.12.6" version = "0.16.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "942c43a6cba1c201dfe81a943c89fa5c9140b34993e0c027f542c80b92e319a7" checksum = "320d34cfe880f2c6243b4cfff8aab3e34eab6325d0a26729f23356418fbdc809"
dependencies = [ dependencies = [
"cargo_metadata 0.15.4", "cargo_metadata 0.18.1",
"cfg-expr 0.12.0", "cfg-expr",
"petgraph", "petgraph",
"semver", "semver",
] ]
@@ -6499,9 +6476,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]] [[package]]
name = "owo-colors" name = "owo-colors"
version = "3.5.0" version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" checksum = "caff54706df99d2a78a5a4e3455ff45448d81ef1bb63c22cd14052ca0e993a3f"
dependencies = [ dependencies = [
"supports-color", "supports-color",
] ]
@@ -8960,11 +8937,11 @@ checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
[[package]] [[package]]
name = "supports-color" name = "supports-color"
version = "1.3.1" version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ba6faf2ca7ee42fdd458f4347ae0a9bd6bcc445ad7cb57ad82b383f18870d6f" checksum = "d6398cde53adc3c4557306a96ce67b302968513830a77a95b2b17305d9719a89"
dependencies = [ dependencies = [
"atty", "is-terminal",
"is_ci", "is_ci",
] ]
@@ -9035,7 +9012,7 @@ version = "6.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a2d580ff6a20c55dfb86be5f9c238f67835d0e81cbdea8bf5680e0897320331" checksum = "2a2d580ff6a20c55dfb86be5f9c238f67835d0e81cbdea8bf5680e0897320331"
dependencies = [ dependencies = [
"cfg-expr 0.15.7", "cfg-expr",
"heck 0.4.1", "heck 0.4.1",
"pkg-config", "pkg-config",
"toml 0.8.10", "toml 0.8.10",
+1 -1
View File
@@ -24,7 +24,7 @@ serde_json = "1.0.79"
toml = { workspace = true } toml = { workspace = true }
fs_extra = "1.2.0" fs_extra = "1.2.0"
cargo_toml = "0.18.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"] } notify = { version = "5.0.0-pre.16", features = ["serde"] }
html_parser = { workspace = true } html_parser = { workspace = true }
cargo_metadata = "0.18.1" cargo_metadata = "0.18.1"
+5
View File
@@ -5,6 +5,7 @@
var protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; var protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
var url = protocol + '//' + window.location.host + '/_dioxus/ws'; var url = protocol + '//' + window.location.host + '/_dioxus/ws';
var poll_interval = 8080; var poll_interval = 8080;
var reload_upon_connect = () => { var reload_upon_connect = () => {
window.setTimeout( window.setTimeout(
() => { () => {
@@ -16,10 +17,14 @@
}; };
var ws = new WebSocket(url); var ws = new WebSocket(url);
ws.onmessage = (ev) => { ws.onmessage = (ev) => {
console.log("Received message: ", ev, ev.data);
if (ev.data == "reload") { if (ev.data == "reload") {
window.location.reload(); window.location.reload();
} }
}; };
ws.onclose = reload_upon_connect; ws.onclose = reload_upon_connect;
})() })()
+1 -1
View File
@@ -64,7 +64,7 @@ impl ExecWithRustFlagsSetter for subprocess::Exec {
/// Build client (WASM). /// Build client (WASM).
/// Note: `rust_flags` argument is only used for the fullstack platform. /// Note: `rust_flags` argument is only used for the fullstack platform.
pub fn build( pub fn build_web(
config: &CrateConfig, config: &CrateConfig,
skip_assets: bool, skip_assets: bool,
rust_flags: Option<String>, rust_flags: Option<String>,
+2 -2
View File
@@ -57,7 +57,7 @@ impl Build {
let build_result = match platform { let build_result = match platform {
Platform::Web => { Platform::Web => {
// `rust_flags` are used by fullstack's client build. // `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 => { Platform::Desktop => {
// Since desktop platform doesn't use `rust_flags`, this // Since desktop platform doesn't use `rust_flags`, this
@@ -80,7 +80,7 @@ impl Build {
} }
None => web_config.features = Some(vec![web_feature]), None => web_config.features = Some(vec![web_feature]),
}; };
crate::builder::build( crate::builder::build_web(
&web_config, &web_config,
self.build.skip_assets, self.build.skip_assets,
Some(client_rust_flags), Some(client_rust_flags),
+1 -1
View File
@@ -138,7 +138,7 @@ async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()
.unwrap() .unwrap()
.map .map
.values() .values()
.filter_map(|(_, template_slot)| *template_slot) .flat_map(|v| v.templates.values().copied())
.collect() .collect()
}; };
for template in templates { for template in templates {
+70 -12
View File
@@ -6,6 +6,7 @@ use dioxus_core::Template;
use dioxus_hot_reload::HotReloadMsg; use dioxus_hot_reload::HotReloadMsg;
use dioxus_html::HtmlCtx; use dioxus_html::HtmlCtx;
use dioxus_rsx::hot_reload::*; use dioxus_rsx::hot_reload::*;
use fs_extra::{dir::CopyOptions, file};
use notify::{RecommendedWatcher, Watcher}; use notify::{RecommendedWatcher, Watcher};
use std::{ use std::{
path::PathBuf, path::PathBuf,
@@ -157,17 +158,9 @@ fn hotreload_files(
for path in &event.paths { for path in &event.paths {
// for various assets that might be linked in, we just try to hotreloading them forcefully // 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.... // That is, unless they appear in an include! macro, in which case we need to a full rebuild....
let Some(ext) = path.extension().and_then(|v| v.to_str()) else {
// 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;
}
continue; continue;
} };
// Workaround for notify and vscode-like editor: // Workaround for notify and vscode-like editor:
// when edit & save a file in vscode, there will be two notifications, // 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) { match rsx_file_map.update_rsx(path, &config.crate_dir) {
Ok(UpdateResult::UpdatedRsx(msgs)) => { Ok(UpdateResult::UpdatedRsx(msgs)) => {
println!("Updated: {:?}", msgs);
messages.extend( messages.extend(
msgs.into_iter() msgs.into_iter()
.map(|msg| HotReloadMsg::UpdateTemplate(msg)), .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 { if *needs_full_rebuild {
// Reset the file map to the new state of the project // Reset the file map to the new state of the project
let FileMapBuildResult { let FileMapBuildResult {
@@ -208,11 +262,15 @@ fn hotreload_files(
} }
*rsx_file_map = new_file_map; *rsx_file_map = new_file_map;
} else {
return;
}
println!("Hot reloading: {:?}", messages);
for msg in messages { for msg in messages {
let _ = hot_reload.messages.send(msg); let _ = hot_reload.messages.send(msg);
} }
}
} }
pub(crate) trait Platform { pub(crate) trait Platform {
+29 -5
View File
@@ -8,6 +8,7 @@ use axum::{
Extension, Extension,
}; };
use dioxus_hot_reload::HotReloadMsg; use dioxus_hot_reload::HotReloadMsg;
use futures_util::{pin_mut, FutureExt};
pub async fn hot_reload_handler( pub async fn hot_reload_handler(
ws: WebSocketUpgrade, ws: WebSocketUpgrade,
@@ -34,9 +35,11 @@ async fn hotreload_loop(mut socket: WebSocket, state: HotReloadState) -> anyhow:
.unwrap() .unwrap()
.map .map
.values() .values()
.filter_map(|(_, template_slot)| *template_slot) .flat_map(|v| v.templates.values().copied())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
println!("previously changed: {:?}", templates);
for template in templates { for template in templates {
socket socket
.send(Message::Text(serde_json::to_string(&template).unwrap())) .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(); let mut rx = state.messages.subscribe();
loop { loop {
if let Ok(msg) = rx.recv().await { let msg = {
let msg = match 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) => { HotReloadMsg::UpdateTemplate(template) => {
Message::Text(serde_json::to_string(&template).unwrap()) Message::Text(serde_json::to_string(&template).unwrap())
} }
HotReloadMsg::UpdateAsset(_) => todo!(), HotReloadMsg::UpdateAsset(asset) => {
Message::Text(format!("asset: {}", asset.display()))
}
HotReloadMsg::Shutdown => todo!(), HotReloadMsg::Shutdown => todo!(),
}
}; };
socket.send(msg).await?; socket.send(msg).await?;
} }
}
Ok(())
} }
+2 -2
View File
@@ -52,7 +52,7 @@ pub async fn serve(
// Since web platform doesn't use `rust_flags`, this argument is explicitly // Since web platform doesn't use `rust_flags`, this argument is explicitly
// set to `None`. // 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 // generate dev-index page
Serve::regen_dev_page(&config, first_build_result.assets.as_ref())?; Serve::regen_dev_page(&config, first_build_result.assets.as_ref())?;
@@ -173,7 +173,7 @@ fn build(
) -> Result<BuildResult> { ) -> Result<BuildResult> {
// Since web platform doesn't use `rust_flags`, this argument is explicitly // Since web platform doesn't use `rust_flags`, this argument is explicitly
// set to `None`. // 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:?}"))?; .map_err(|e| anyhow::anyhow!("Build failed: {e:?}"))?;
// change the websocket reload state to true; // change the websocket reload state to true;
+2
View File
@@ -534,6 +534,7 @@ impl VirtualDom {
#[instrument(skip(self), level = "trace", name = "VirtualDom::replace_template")] #[instrument(skip(self), level = "trace", name = "VirtualDom::replace_template")]
pub fn replace_template(&mut self, template: Template) { pub fn replace_template(&mut self, template: Template) {
self.register_template_first_byte_index(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 // 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(); let mut dirty = Vec::new();
for (id, scope) in self.scopes.iter() { for (id, scope) in self.scopes.iter() {
@@ -545,6 +546,7 @@ impl VirtualDom {
} }
} }
} }
for dirty in dirty { for dirty in dirty {
self.mark_dirty(dirty); self.mark_dirty(dirty);
} }
+31 -34
View File
@@ -119,7 +119,10 @@ pub fn init<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
phantom: _, phantom: _,
} = cfg; } = cfg;
if let Ok(crate_dir) = PathBuf::from_str(root_path) { let Ok(crate_dir) = PathBuf::from_str(root_path) else {
return;
};
// try to find the gitignore file // try to find the gitignore file
let gitignore_file_path = crate_dir.join(".gitignore"); let gitignore_file_path = crate_dir.join(".gitignore");
let (gitignore, _) = ignore::gitignore::Gitignore::new(gitignore_file_path); let (gitignore, _) = ignore::gitignore::Gitignore::new(gitignore_file_path);
@@ -163,8 +166,14 @@ pub fn init<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
} }
} }
match LocalSocketListener::bind(hot_reload_socket_path) { let local_socket_stream = match LocalSocketListener::bind(hot_reload_socket_path) {
Ok(local_socket_stream) => { 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)); let aborted = Arc::new(Mutex::new(false));
// listen for connections // listen for connections
@@ -183,14 +192,12 @@ pub fn init<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
.unwrap() .unwrap()
.map .map
.values() .values()
.filter_map(|(_, template_slot)| *template_slot) .flat_map(|v| v.templates.values().copied())
.collect() .collect()
}; };
for template in templates { for template in templates {
if !send_msg( if !send_msg(HotReloadMsg::UpdateTemplate(template), &mut connection) {
HotReloadMsg::UpdateTemplate(template),
&mut connection,
) {
continue; continue;
} }
} }
@@ -212,16 +219,13 @@ pub fn init<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
let (tx, rx) = std::sync::mpsc::channel(); let (tx, rx) = std::sync::mpsc::channel();
let mut watcher = let mut watcher = RecommendedWatcher::new(tx, notify::Config::default()).unwrap();
RecommendedWatcher::new(tx, notify::Config::default()).unwrap();
for path in listening_paths { for path in listening_paths {
let full_path = crate_dir.join(path); let full_path = crate_dir.join(path);
if let Err(err) = watcher.watch(&full_path, RecursiveMode::Recursive) { if let Err(err) = watcher.watch(&full_path, RecursiveMode::Recursive) {
if log { if log {
println!( println!("hot reloading failed to start watching {full_path:?}:\n{err:?}",);
"hot reloading failed to start watching {full_path:?}:\n{err:?}",
);
} }
} }
} }
@@ -246,17 +250,21 @@ pub fn init<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
return shutdown; return shutdown;
} else if log { } else if log {
println!( println!("Rebuild needed... shutting down hot reloading.\nManually rebuild the application to view further changes.");
"Rebuild needed... shutting down hot reloading.\nManually rebuild the application to view further changes."
);
} }
true true
} }
}; };
for evt in rx { for evt in rx {
if chrono::Local::now().timestamp_millis() >= last_update_time { if chrono::Local::now().timestamp_millis() < last_update_time {
if let Ok(evt) = evt { continue;
}
let Ok(evt) = evt else {
continue;
};
let real_paths = evt let real_paths = evt
.paths .paths
.iter() .iter()
@@ -283,9 +291,7 @@ pub fn init<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
let mut channels = channels.lock().unwrap(); let mut channels = channels.lock().unwrap();
for path in real_paths { for path in real_paths {
// if this file type cannot be hot reloaded, rebuild the application // if this file type cannot be hot reloaded, rebuild the application
if path.extension().and_then(|p| p.to_str()) != Some("rs") if path.extension().and_then(|p| p.to_str()) != Some("rs") && rebuild() {
&& rebuild()
{
return; return;
} }
// find changes to the rsx in the file // find changes to the rsx in the file
@@ -299,10 +305,7 @@ pub fn init<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
let mut i = 0; let mut i = 0;
while i < channels.len() { while i < channels.len() {
let channel = &mut channels[i]; let channel = &mut channels[i];
if send_msg( if send_msg(HotReloadMsg::UpdateTemplate(msg), channel) {
HotReloadMsg::UpdateTemplate(msg),
channel,
) {
i += 1; i += 1;
} else { } else {
channels.remove(i); channels.remove(i);
@@ -310,6 +313,7 @@ pub fn init<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
} }
} }
} }
Ok(UpdateResult::NeedsRebuild) => { Ok(UpdateResult::NeedsRebuild) => {
drop(channels); drop(channels);
if rebuild() { if rebuild() {
@@ -319,22 +323,15 @@ pub fn init<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
} }
Err(err) => { Err(err) => {
if log { if log {
println!( println!("hot reloading failed to update rsx:\n{err:?}");
"hot reloading failed to update rsx:\n{err:?}"
);
}
} }
} }
} }
} }
last_update_time = chrono::Local::now().timestamp_millis(); last_update_time = chrono::Local::now().timestamp_millis();
} }
}
}); });
}
Err(error) => println!("failed to connect to hot reloading\n{error}"),
}
}
} }
fn send_msg(msg: HotReloadMsg, channel: &mut impl Write) -> bool { fn send_msg(msg: HotReloadMsg, channel: &mut impl Write) -> bool {
+28 -13
View File
@@ -3,8 +3,22 @@ use quote::ToTokens;
use syn::{File, Macro}; use syn::{File, Macro};
pub enum DiffResult { pub enum DiffResult {
/// Non-rsx was changed in the file
CodeChanged, 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. /// 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; return DiffResult::CodeChanged;
} }
for (new, old) in new.items.iter().zip(old.items.iter()) { for (new, old) in new.items.iter().zip(old.items.iter()) {
if find_rsx_item(new, old, &mut rsx_calls) { if find_rsx_item(new, old, &mut rsx_calls) {
tracing::trace!( tracing::trace!(
@@ -34,15 +49,12 @@ pub fn find_rsx(new: &File, old: &File) -> DiffResult {
return DiffResult::CodeChanged; return DiffResult::CodeChanged;
} }
} }
tracing::trace!("found hot reload-able changes {:#?}", rsx_calls); tracing::trace!("found hot reload-able changes {:#?}", rsx_calls);
DiffResult::RsxChanged(rsx_calls) DiffResult::RsxChanged { rsx_calls }
} }
fn find_rsx_item( fn find_rsx_item(new: &syn::Item, old: &syn::Item, rsx_calls: &mut Vec<ChangedRsx>) -> bool {
new: &syn::Item,
old: &syn::Item,
rsx_calls: &mut Vec<(Macro, TokenStream)>,
) -> bool {
match (new, old) { match (new, old) {
(syn::Item::Const(new_item), syn::Item::Const(old_item)) => { (syn::Item::Const(new_item), syn::Item::Const(old_item)) => {
find_rsx_expr(&new_item.expr, &old_item.expr, rsx_calls) find_rsx_expr(&new_item.expr, &old_item.expr, rsx_calls)
@@ -190,7 +202,7 @@ fn find_rsx_item(
fn find_rsx_trait( fn find_rsx_trait(
new_item: &syn::ItemTrait, new_item: &syn::ItemTrait,
old_item: &syn::ItemTrait, old_item: &syn::ItemTrait,
rsx_calls: &mut Vec<(Macro, TokenStream)>, rsx_calls: &mut Vec<ChangedRsx>,
) -> bool { ) -> bool {
if new_item.items.len() != old_item.items.len() { if new_item.items.len() != old_item.items.len() {
return true; return true;
@@ -243,7 +255,7 @@ fn find_rsx_trait(
fn find_rsx_block( fn find_rsx_block(
new_block: &syn::Block, new_block: &syn::Block,
old_block: &syn::Block, old_block: &syn::Block,
rsx_calls: &mut Vec<(Macro, TokenStream)>, rsx_calls: &mut Vec<ChangedRsx>,
) -> bool { ) -> bool {
if new_block.stmts.len() != old_block.stmts.len() { if new_block.stmts.len() != old_block.stmts.len() {
return true; return true;
@@ -259,7 +271,7 @@ fn find_rsx_block(
fn find_rsx_stmt( fn find_rsx_stmt(
new_stmt: &syn::Stmt, new_stmt: &syn::Stmt,
old_stmt: &syn::Stmt, old_stmt: &syn::Stmt,
rsx_calls: &mut Vec<(Macro, TokenStream)>, rsx_calls: &mut Vec<ChangedRsx>,
) -> bool { ) -> bool {
match (new_stmt, old_stmt) { match (new_stmt, old_stmt) {
(syn::Stmt::Local(new_local), syn::Stmt::Local(old_local)) => { (syn::Stmt::Local(new_local), syn::Stmt::Local(old_local)) => {
@@ -293,7 +305,7 @@ fn find_rsx_stmt(
fn find_rsx_expr( fn find_rsx_expr(
new_expr: &syn::Expr, new_expr: &syn::Expr,
old_expr: &syn::Expr, old_expr: &syn::Expr,
rsx_calls: &mut Vec<(Macro, TokenStream)>, rsx_calls: &mut Vec<ChangedRsx>,
) -> bool { ) -> bool {
match (new_expr, old_expr) { match (new_expr, old_expr) {
(syn::Expr::Array(new_expr), syn::Expr::Array(old_expr)) => { (syn::Expr::Array(new_expr), syn::Expr::Array(old_expr)) => {
@@ -631,7 +643,7 @@ fn find_rsx_expr(
fn find_rsx_macro( fn find_rsx_macro(
new_mac: &syn::Macro, new_mac: &syn::Macro,
old_mac: &syn::Macro, old_mac: &syn::Macro,
rsx_calls: &mut Vec<(Macro, TokenStream)>, rsx_calls: &mut Vec<ChangedRsx>,
) -> bool { ) -> bool {
if matches!( if matches!(
new_mac new_mac
@@ -648,7 +660,10 @@ fn find_rsx_macro(
.as_deref(), .as_deref(),
Some("rsx" | "render") 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 false
} else { } else {
new_mac != old_mac new_mac != old_mac
@@ -1,21 +1,32 @@
use crate::{CallBody, HotReloadingContext}; use crate::{CallBody, HotReloadingContext};
use dioxus_core::Template; use dioxus_core::{
prelude::{TemplateAttribute, TemplateNode},
Template,
};
use krates::cm::MetadataCommand; use krates::cm::MetadataCommand;
use krates::Cmd; use krates::Cmd;
pub use proc_macro2::TokenStream; pub use proc_macro2::TokenStream;
pub use std::collections::HashMap; pub use std::collections::HashMap;
use std::path::PathBuf;
pub use std::sync::Mutex; pub use std::sync::Mutex;
pub use std::time::SystemTime; 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, io, path::Path};
pub use std::{fs::File, io::Read}; pub use std::{fs::File, io::Read};
pub use syn::__private::ToTokens; pub use syn::__private::ToTokens;
use syn::spanned::Spanned; use syn::spanned::Spanned;
use super::hot_reload_diff::{find_rsx, DiffResult}; use super::{
hot_reload_diff::{find_rsx, DiffResult},
ChangedRsx,
};
pub enum UpdateResult { pub enum UpdateResult {
UpdatedRsx(Vec<Template>), UpdatedRsx(Vec<Template>),
NeedsRebuild, NeedsRebuild,
} }
@@ -23,23 +34,27 @@ pub enum UpdateResult {
pub struct FileMapBuildResult<Ctx: HotReloadingContext> { pub struct FileMapBuildResult<Ctx: HotReloadingContext> {
/// The FileMap that was built /// The FileMap that was built
pub map: FileMap<Ctx>, pub map: FileMap<Ctx>,
/// Any errors that occurred while building the FileMap that were not fatal /// Any errors that occurred while building the FileMap that were not fatal
pub errors: Vec<io::Error>, pub errors: Vec<io::Error>,
} }
pub struct FileMap<Ctx: HotReloadingContext> { pub struct FileMap<Ctx: HotReloadingContext> {
pub map: HashMap<PathBuf, (String, Option<Template>)>, pub map: HashMap<PathBuf, CachedSynFile>,
in_workspace: HashMap<PathBuf, Option<PathBuf>>, in_workspace: HashMap<PathBuf, Option<PathBuf>>,
phantom: std::marker::PhantomData<Ctx>, phantom: std::marker::PhantomData<Ctx>,
} }
struct CachedSynFile { /// A cached file that has been parsed
raw: String, ///
file: syn::File, /// We store the templates found in this file
path: PathBuf, pub struct CachedSynFile {
template: Option<Template>, pub raw: String,
pub path: PathBuf,
pub templates: HashMap<&'static str, Template>,
pub tracked_assets: HashSet<PathBuf>,
} }
impl<Ctx: HotReloadingContext> FileMap<Ctx> { impl<Ctx: HotReloadingContext> FileMap<Ctx> {
@@ -53,16 +68,262 @@ impl<Ctx: HotReloadingContext> FileMap<Ctx> {
path: PathBuf, path: PathBuf,
mut filter: impl FnMut(&Path) -> bool, mut filter: impl FnMut(&Path) -> bool,
) -> io::Result<FileMapBuildResult<Ctx>> { ) -> io::Result<FileMapBuildResult<Ctx>> {
struct FileMapSearchResult { let FileMapSearchResult { map, errors } = find_rs_files(path, &mut filter);
map: HashMap<PathBuf, (String, Option<Template>)>,
errors: Vec<io::Error>, let file_map_build_result = FileMapBuildResult {
errors,
map: Self {
map,
in_workspace: HashMap::new(),
phantom: std::marker::PhantomData,
},
};
Ok(file_map_build_result)
} }
fn find_rs_files(
root: PathBuf, /// Try to update the rsx in a file
filter: &mut impl FnMut(&Path) -> bool, pub fn update_rsx(
) -> FileMapSearchResult { &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 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);
};
// 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)?;
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));
}
}
_ => {}
}
}
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 {
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()
}
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>> {
if let Some(in_workspace) = self.in_workspace.get(crate_dir) {
Ok(in_workspace.clone())
} else {
let mut cmd = Cmd::new();
let manafest_path = crate_dir.join("Cargo.toml");
cmd.manifest_path(&manafest_path);
let cmd: MetadataCommand = cmd.into();
let metadata = cmd
.exec()
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;
let in_workspace = metadata.workspace_root != crate_dir;
let workspace_path = in_workspace.then(|| metadata.workspace_root.into());
self.in_workspace
.insert(crate_dir.to_path_buf(), workspace_path.clone());
Ok(workspace_path)
}
}
}
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 files = HashMap::new();
let mut errors = Vec::new(); let mut errors = Vec::new();
if root.is_dir() { if root.is_dir() {
let read_dir = match fs::read_dir(root) { let read_dir = match fs::read_dir(root) {
Ok(read_dir) => read_dir, Ok(read_dir) => read_dir,
@@ -87,7 +348,13 @@ impl<Ctx: HotReloadingContext> FileMap<Ctx> {
let mut src = String::new(); let mut src = String::new();
match file.read_to_string(&mut src) { match file.read_to_string(&mut src) {
Ok(_) => { Ok(_) => {
files.insert(root, (src, None)); let cached_file = CachedSynFile {
raw: src.clone(),
path: root.clone(),
templates: HashMap::new(),
tracked_assets: HashSet::new(),
};
files.insert(root, cached_file);
} }
Err(err) => { Err(err) => {
errors.push(err); errors.push(err);
@@ -95,123 +362,29 @@ impl<Ctx: HotReloadingContext> FileMap<Ctx> {
} }
} }
} }
FileMapSearchResult { map: files, errors } FileMapSearchResult { map: files, errors }
} }
let FileMapSearchResult { map, errors } = find_rs_files(path, &mut filter); #[derive(Debug)]
let result = Self { pub enum HotreloadError {
map, Failure(io::Error),
in_workspace: HashMap::new(), Parse,
phantom: std::marker::PhantomData, NoPreviousBuild,
}; }
Ok(FileMapBuildResult {
map: result,
errors,
})
}
/// Try to update the rsx in a file impl std::fmt::Display for HotreloadError {
pub fn update_rsx(&mut self, file_path: &Path, crate_dir: &Path) -> io::Result<UpdateResult> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut file = File::open(file_path)?; match self {
let mut src = String::new(); Self::Failure(err) => write!(f, "Failed to parse file: {}", err),
file.read_to_string(&mut src)?; Self::Parse => write!(f, "Failed to parse file"),
Self::NoPreviousBuild => write!(f, "No previous build found"),
// 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 {
return Ok(UpdateResult::NeedsRebuild);
};
let in_workspace = self.child_in_workspace(crate_dir)?;
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);
}
}
}
}
return Ok(UpdateResult::UpdatedRsx(messages));
}
}
}
} 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;
}
Ok(UpdateResult::NeedsRebuild)
}
fn child_in_workspace(&mut self, crate_dir: &Path) -> io::Result<Option<PathBuf>> {
if let Some(in_workspace) = self.in_workspace.get(crate_dir) {
Ok(in_workspace.clone())
} else {
let mut cmd = Cmd::new();
let manafest_path = crate_dir.join("Cargo.toml");
cmd.manifest_path(&manafest_path);
let cmd: MetadataCommand = cmd.into();
let metadata = cmd
.exec()
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;
let in_workspace = metadata.workspace_root != crate_dir;
let workspace_path = in_workspace.then(|| metadata.workspace_root.into());
self.in_workspace
.insert(crate_dir.to_path_buf(), workspace_path.clone());
Ok(workspace_path)
} }
} }
} }
impl From<io::Error> for HotreloadError {
fn from(err: io::Error) -> Self {
HotreloadError::Failure(err)
}
}
+1
View File
@@ -74,6 +74,7 @@ impl CallBody {
roots: &self.roots, roots: &self.roots,
location: None, location: None,
}; };
renderer.update_template::<Ctx>(template, location) renderer.update_template::<Ctx>(template, location)
} }
+12 -20
View File
@@ -24,7 +24,11 @@ wasm-bindgen-futures = "0.4.29"
tracing = { workspace = true } tracing = { workspace = true }
rustc-hash = { workspace = true } rustc-hash = { workspace = true }
console_error_panic_hook = { version = "0.1.7", optional = 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 } futures-channel = { workspace = true }
serde_json = { version = "1.0" } serde_json = { version = "1.0" }
serde = { version = "1.0" } serde = { version = "1.0" }
@@ -43,35 +47,23 @@ features = [
"Text", "Text",
"Window", "Window",
"DataTransfer", "DataTransfer",
"console" "console",
"NodeList",
] ]
[features] [features]
default = ["panic_hook", "mounted", "file_engine", "hot_reload", "eval"] default = ["panic_hook", "mounted", "file_engine", "hot_reload", "eval"]
panic_hook = ["console_error_panic_hook"] panic_hook = ["console_error_panic_hook"]
hydrate = [ hydrate = ["web-sys/Comment"]
"web-sys/Comment", mounted = ["web-sys/Element", "dioxus-html/mounted"]
]
mounted = [
"web-sys/Element",
"dioxus-html/mounted"
]
file_engine = [ file_engine = [
"web-sys/File", "web-sys/File",
"web-sys/FileList", "web-sys/FileList",
"web-sys/FileReader", "web-sys/FileReader",
"async-trait" "async-trait",
]
hot_reload = [
"web-sys/MessageEvent",
"web-sys/WebSocket",
"web-sys/Location",
]
eval = [
"dioxus-html/eval",
"serde-wasm-bindgen",
"async-trait"
] ]
hot_reload = ["web-sys/MessageEvent", "web-sys/WebSocket", "web-sys/Location"]
eval = ["dioxus-html/eval", "serde-wasm-bindgen", "async-trait"]
[dev-dependencies] [dev-dependencies]
dioxus = { workspace = true } dioxus = { workspace = true }
+27 -1
View File
@@ -3,6 +3,7 @@
use futures_channel::mpsc::UnboundedReceiver; use futures_channel::mpsc::UnboundedReceiver;
use dioxus_core::Template; use dioxus_core::Template;
use web_sys::{console, Element};
pub(crate) fn init() -> UnboundedReceiver<Template> { pub(crate) fn init() -> UnboundedReceiver<Template> {
use wasm_bindgen::closure::Closure; use wasm_bindgen::closure::Closure;
@@ -29,14 +30,39 @@ pub(crate) fn init() -> UnboundedReceiver<Template> {
// change the rsx when new data is received // change the rsx when new data is received
let cl = Closure::wrap(Box::new(move |e: MessageEvent| { 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>() { if let Ok(text) = e.data().dyn_into::<js_sys::JsString>() {
let string: String = text.into(); let string: String = text.into();
let val = serde_json::from_str::<serde_json::Value>(&string).unwrap();
if let Ok(val) = serde_json::from_str::<serde_json::Value>(&string) {
// leak the value // leak the value
let val: &'static serde_json::Value = Box::leak(Box::new(val)); let val: &'static serde_json::Value = Box::leak(Box::new(val));
let template: Template = Template::deserialize(val).unwrap(); let template: Template = Template::deserialize(val).unwrap();
tx.unbounded_send(template).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)>); }) as Box<dyn FnMut(MessageEvent)>);
ws.set_onmessage(Some(cl.as_ref().unchecked_ref())); ws.set_onmessage(Some(cl.as_ref().unchecked_ref()));