fix: hotreloading files with multiple templates, asset hotreloading
This commit is contained in:
16 files changed
+723
-453
No files matched your search
Generated
+16
-39
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
})()
|
||||
@@ -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>,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
let Ok(crate_dir) = PathBuf::from_str(root_path) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// convert the excluded paths to absolute paths
|
||||
let excluded_paths = excluded_paths
|
||||
.iter()
|
||||
.map(|path| crate_dir.join(PathBuf::from(path)))
|
||||
.collect::<Vec<_>>();
|
||||
// try to find the gitignore file
|
||||
let gitignore_file_path = crate_dir.join(".gitignore");
|
||||
let (gitignore, _) = ignore::gitignore::Gitignore::new(gitignore_file_path);
|
||||
|
||||
let channels = Arc::new(Mutex::new(Vec::new()));
|
||||
let FileMapBuildResult {
|
||||
map: file_map,
|
||||
errors,
|
||||
} = FileMap::<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));
|
||||
// convert the excluded paths to absolute paths
|
||||
let excluded_paths = excluded_paths
|
||||
.iter()
|
||||
.map(|path| crate_dir.join(PathBuf::from(path)))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let target_dir = crate_dir.join("target");
|
||||
let hot_reload_socket_path = target_dir.join("dioxusin");
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
// On unix, if you force quit the application, it can leave the file socket open
|
||||
// This will cause the local socket listener to fail to open
|
||||
// We check if the file socket is already open from an old session and then delete it
|
||||
if hot_reload_socket_path.exists() {
|
||||
let _ = std::fs::remove_file(hot_reload_socket_path.clone());
|
||||
}
|
||||
}
|
||||
|
||||
match LocalSocketListener::bind(hot_reload_socket_path) {
|
||||
Ok(local_socket_stream) => {
|
||||
let aborted = Arc::new(Mutex::new(false));
|
||||
|
||||
// listen for connections
|
||||
std::thread::spawn({
|
||||
let file_map = file_map.clone();
|
||||
let channels = channels.clone();
|
||||
let aborted = aborted.clone();
|
||||
let _ = local_socket_stream.set_nonblocking(true);
|
||||
move || {
|
||||
loop {
|
||||
if let Ok(mut connection) = local_socket_stream.accept() {
|
||||
// send any templates than have changed before the socket connected
|
||||
let templates: Vec<_> = {
|
||||
file_map
|
||||
.lock()
|
||||
.unwrap()
|
||||
.map
|
||||
.values()
|
||||
.filter_map(|(_, template_slot)| *template_slot)
|
||||
.collect()
|
||||
};
|
||||
for template in templates {
|
||||
if !send_msg(
|
||||
HotReloadMsg::UpdateTemplate(template),
|
||||
&mut connection,
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
channels.lock().unwrap().push(connection);
|
||||
if log {
|
||||
println!("Connected to hot reloading 🚀");
|
||||
}
|
||||
}
|
||||
if *aborted.lock().unwrap() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// watch for changes
|
||||
std::thread::spawn(move || {
|
||||
let mut last_update_time = chrono::Local::now().timestamp();
|
||||
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
|
||||
let mut watcher =
|
||||
RecommendedWatcher::new(tx, notify::Config::default()).unwrap();
|
||||
|
||||
for path in listening_paths {
|
||||
let full_path = crate_dir.join(path);
|
||||
if let Err(err) = watcher.watch(&full_path, RecursiveMode::Recursive) {
|
||||
if log {
|
||||
println!(
|
||||
"hot reloading failed to start watching {full_path:?}:\n{err:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut rebuild = {
|
||||
let aborted = aborted.clone();
|
||||
let channels = channels.clone();
|
||||
move || {
|
||||
if let Some(rebuild_callback) = &mut rebuild_with {
|
||||
if log {
|
||||
println!("Rebuilding the application...");
|
||||
}
|
||||
let shutdown = rebuild_callback();
|
||||
|
||||
if shutdown {
|
||||
*aborted.lock().unwrap() = true;
|
||||
}
|
||||
|
||||
for channel in &mut *channels.lock().unwrap() {
|
||||
send_msg(HotReloadMsg::Shutdown, channel);
|
||||
}
|
||||
|
||||
return shutdown;
|
||||
} else if log {
|
||||
println!(
|
||||
"Rebuild needed... shutting down hot reloading.\nManually rebuild the application to view further changes."
|
||||
);
|
||||
}
|
||||
true
|
||||
}
|
||||
};
|
||||
|
||||
for evt in rx {
|
||||
if chrono::Local::now().timestamp_millis() >= last_update_time {
|
||||
if let Ok(evt) = evt {
|
||||
let real_paths = evt
|
||||
.paths
|
||||
.iter()
|
||||
.filter(|path| {
|
||||
// skip non rust files
|
||||
matches!(
|
||||
path.extension().and_then(|p| p.to_str()),
|
||||
Some("rs" | "toml" | "css" | "html" | "js")
|
||||
) &&
|
||||
// skip excluded paths
|
||||
!excluded_paths.iter().any(|p| path.starts_with(p)) &&
|
||||
// respect .gitignore
|
||||
!gitignore
|
||||
.matched_path_or_any_parents(path, false)
|
||||
.is_ignore()
|
||||
})
|
||||
.collect::<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 mut channels = channels.lock().unwrap();
|
||||
for path in real_paths {
|
||||
// if this file type cannot be hot reloaded, rebuild the application
|
||||
if path.extension().and_then(|p| p.to_str()) != Some("rs")
|
||||
&& rebuild()
|
||||
{
|
||||
return;
|
||||
}
|
||||
// find changes to the rsx in the file
|
||||
match file_map
|
||||
.lock()
|
||||
.unwrap()
|
||||
.update_rsx(path, crate_dir.as_path())
|
||||
{
|
||||
Ok(UpdateResult::UpdatedRsx(msgs)) => {
|
||||
for msg in msgs {
|
||||
let mut i = 0;
|
||||
while i < channels.len() {
|
||||
let channel = &mut channels[i];
|
||||
if send_msg(
|
||||
HotReloadMsg::UpdateTemplate(msg),
|
||||
channel,
|
||||
) {
|
||||
i += 1;
|
||||
} else {
|
||||
channels.remove(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(UpdateResult::NeedsRebuild) => {
|
||||
drop(channels);
|
||||
if rebuild() {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
Err(err) => {
|
||||
if log {
|
||||
println!(
|
||||
"hot reloading failed to update rsx:\n{err:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
last_update_time = chrono::Local::now().timestamp_millis();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(error) => println!("failed to connect to hot reloading\n{error}"),
|
||||
let channels = Arc::new(Mutex::new(Vec::new()));
|
||||
let FileMapBuildResult {
|
||||
map: file_map,
|
||||
errors,
|
||||
} = FileMap::<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 local_socket_stream = match LocalSocketListener::bind(hot_reload_socket_path) {
|
||||
Ok(local_socket_stream) => local_socket_stream,
|
||||
Err(err) => {
|
||||
println!("failed to connect to hot reloading\n{err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let aborted = Arc::new(Mutex::new(false));
|
||||
|
||||
// listen for connections
|
||||
std::thread::spawn({
|
||||
let file_map = file_map.clone();
|
||||
let channels = channels.clone();
|
||||
let aborted = aborted.clone();
|
||||
let _ = local_socket_stream.set_nonblocking(true);
|
||||
move || {
|
||||
loop {
|
||||
if let Ok(mut connection) = local_socket_stream.accept() {
|
||||
// send any templates than have changed before the socket connected
|
||||
let templates: Vec<_> = {
|
||||
file_map
|
||||
.lock()
|
||||
.unwrap()
|
||||
.map
|
||||
.values()
|
||||
.flat_map(|v| v.templates.values().copied())
|
||||
.collect()
|
||||
};
|
||||
|
||||
for template in templates {
|
||||
if !send_msg(HotReloadMsg::UpdateTemplate(template), &mut connection) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
channels.lock().unwrap().push(connection);
|
||||
if log {
|
||||
println!("Connected to hot reloading 🚀");
|
||||
}
|
||||
}
|
||||
if *aborted.lock().unwrap() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// watch for changes
|
||||
std::thread::spawn(move || {
|
||||
let mut last_update_time = chrono::Local::now().timestamp();
|
||||
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
|
||||
let mut watcher = RecommendedWatcher::new(tx, notify::Config::default()).unwrap();
|
||||
|
||||
for path in listening_paths {
|
||||
let full_path = crate_dir.join(path);
|
||||
if let Err(err) = watcher.watch(&full_path, RecursiveMode::Recursive) {
|
||||
if log {
|
||||
println!("hot reloading failed to start watching {full_path:?}:\n{err:?}",);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut rebuild = {
|
||||
let aborted = aborted.clone();
|
||||
let channels = channels.clone();
|
||||
move || {
|
||||
if let Some(rebuild_callback) = &mut rebuild_with {
|
||||
if log {
|
||||
println!("Rebuilding the application...");
|
||||
}
|
||||
let shutdown = rebuild_callback();
|
||||
|
||||
if shutdown {
|
||||
*aborted.lock().unwrap() = true;
|
||||
}
|
||||
|
||||
for channel in &mut *channels.lock().unwrap() {
|
||||
send_msg(HotReloadMsg::Shutdown, channel);
|
||||
}
|
||||
|
||||
return shutdown;
|
||||
} else if log {
|
||||
println!("Rebuild needed... shutting down hot reloading.\nManually rebuild the application to view further changes.");
|
||||
}
|
||||
true
|
||||
}
|
||||
};
|
||||
|
||||
for evt in rx {
|
||||
if chrono::Local::now().timestamp_millis() < last_update_time {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Ok(evt) = evt else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let real_paths = evt
|
||||
.paths
|
||||
.iter()
|
||||
.filter(|path| {
|
||||
// skip non rust files
|
||||
matches!(
|
||||
path.extension().and_then(|p| p.to_str()),
|
||||
Some("rs" | "toml" | "css" | "html" | "js")
|
||||
) &&
|
||||
// skip excluded paths
|
||||
!excluded_paths.iter().any(|p| path.starts_with(p)) &&
|
||||
// respect .gitignore
|
||||
!gitignore
|
||||
.matched_path_or_any_parents(path, false)
|
||||
.is_ignore()
|
||||
})
|
||||
.collect::<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 mut channels = channels.lock().unwrap();
|
||||
for path in real_paths {
|
||||
// if this file type cannot be hot reloaded, rebuild the application
|
||||
if path.extension().and_then(|p| p.to_str()) != Some("rs") && rebuild() {
|
||||
return;
|
||||
}
|
||||
// find changes to the rsx in the file
|
||||
match file_map
|
||||
.lock()
|
||||
.unwrap()
|
||||
.update_rsx(path, crate_dir.as_path())
|
||||
{
|
||||
Ok(UpdateResult::UpdatedRsx(msgs)) => {
|
||||
for msg in msgs {
|
||||
let mut i = 0;
|
||||
while i < channels.len() {
|
||||
let channel = &mut channels[i];
|
||||
if send_msg(HotReloadMsg::UpdateTemplate(msg), channel) {
|
||||
i += 1;
|
||||
} else {
|
||||
channels.remove(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(UpdateResult::NeedsRebuild) => {
|
||||
drop(channels);
|
||||
if rebuild() {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
Err(err) => {
|
||||
if log {
|
||||
println!("hot reloading failed to update rsx:\n{err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
last_update_time = chrono::Local::now().timestamp_millis();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn send_msg(msg: HotReloadMsg, channel: &mut impl Write) -> bool {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
return Ok(UpdateResult::NeedsRebuild);
|
||||
};
|
||||
let syntax = syn::parse_file(&src).map_err(|_err| HotreloadError::Parse)?;
|
||||
|
||||
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 {
|
||||
// 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::create(crate_dir.to_path_buf())?;
|
||||
let FileMapBuildResult { map, mut errors } =
|
||||
FileMap::<Ctx>::create(crate_dir.to_path_buf())?;
|
||||
|
||||
if let Some(err) = errors.pop() {
|
||||
return Err(err);
|
||||
return Err(HotreloadError::Failure(err));
|
||||
}
|
||||
*self = map;
|
||||
|
||||
// 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::NeedsRebuild)
|
||||
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>> {
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -74,6 +74,7 @@ impl CallBody {
|
||||
roots: &self.roots,
|
||||
location: None,
|
||||
};
|
||||
|
||||
renderer.update_template::<Ctx>(template, location)
|
||||
}
|
||||
|
||||
|
||||
+12
-20
@@ -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 }
|
||||
|
||||
@@ -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)>);
|
||||
|
||||
|
||||
Reference in new issue
Block a user