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
+723 -453

No files matched your search

Generated
+16 -39
View File
@@ -1247,20 +1247,6 @@ dependencies = [
"serde",
]
[[package]]
name = "cargo_metadata"
version = "0.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a"
dependencies = [
"camino",
"cargo-platform",
"semver",
"serde",
"serde_json",
"thiserror",
]
[[package]]
name = "cargo_metadata"
version = "0.17.0"
@@ -1347,15 +1333,6 @@ dependencies = [
"uuid",
]
[[package]]
name = "cfg-expr"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bbc13bf6290a6b202cc3efb36f7ec2b739a80634215630c8053a313edf6abef"
dependencies = [
"smallvec",
]
[[package]]
name = "cfg-expr"
version = "0.15.7"
@@ -2246,7 +2223,7 @@ dependencies = [
"pretty_assertions",
"proc-macro2",
"quote",
"syn 1.0.109",
"syn 2.0.51",
]
[[package]]
@@ -2284,7 +2261,7 @@ dependencies = [
"hyper-util",
"ignore",
"indicatif",
"interprocess-docfix",
"interprocess",
"lazy_static",
"log",
"manganis-cli-support",
@@ -2511,7 +2488,7 @@ dependencies = [
"dioxus-rsx",
"execute",
"ignore",
"interprocess-docfix",
"interprocess",
"notify",
"once_cell",
"serde",
@@ -5028,10 +5005,10 @@ dependencies = [
]
[[package]]
name = "interprocess-docfix"
version = "1.2.2"
name = "interprocess"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b84ee245c606aeb0841649a9288e3eae8c61b853a8cd5c0e14450e96d53d28f"
checksum = "81f2533f3be42fffe3b5e63b71aeca416c1c3bc33e4e27be018521e76b1f38fb"
dependencies = [
"blocking",
"cfg-if",
@@ -5290,12 +5267,12 @@ dependencies = [
[[package]]
name = "krates"
version = "0.12.6"
version = "0.16.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "942c43a6cba1c201dfe81a943c89fa5c9140b34993e0c027f542c80b92e319a7"
checksum = "320d34cfe880f2c6243b4cfff8aab3e34eab6325d0a26729f23356418fbdc809"
dependencies = [
"cargo_metadata 0.15.4",
"cfg-expr 0.12.0",
"cargo_metadata 0.18.1",
"cfg-expr",
"petgraph",
"semver",
]
@@ -6499,9 +6476,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "owo-colors"
version = "3.5.0"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
checksum = "caff54706df99d2a78a5a4e3455ff45448d81ef1bb63c22cd14052ca0e993a3f"
dependencies = [
"supports-color",
]
@@ -8960,11 +8937,11 @@ checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
[[package]]
name = "supports-color"
version = "1.3.1"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ba6faf2ca7ee42fdd458f4347ae0a9bd6bcc445ad7cb57ad82b383f18870d6f"
checksum = "d6398cde53adc3c4557306a96ce67b302968513830a77a95b2b17305d9719a89"
dependencies = [
"atty",
"is-terminal",
"is_ci",
]
@@ -9035,7 +9012,7 @@ version = "6.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a2d580ff6a20c55dfb86be5f9c238f67835d0e81cbdea8bf5680e0897320331"
dependencies = [
"cfg-expr 0.15.7",
"cfg-expr",
"heck 0.4.1",
"pkg-config",
"toml 0.8.10",
+1 -1
View File
@@ -24,7 +24,7 @@ serde_json = "1.0.79"
toml = { workspace = true }
fs_extra = "1.2.0"
cargo_toml = "0.18.0"
futures-util = { workspace = true }
futures-util = { workspace = true, features = ["async-await-macro"] }
notify = { version = "5.0.0-pre.16", features = ["serde"] }
html_parser = { workspace = true }
cargo_metadata = "0.18.1"
+11 -6
View File
@@ -4,7 +4,8 @@
(function () {
var protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
var url = protocol + '//' + window.location.host + '/_dioxus/ws';
var poll_interval = 8080;
var poll_interval = 8080;
var reload_upon_connect = () => {
window.setTimeout(
() => {
@@ -15,11 +16,15 @@
poll_interval);
};
var ws = new WebSocket(url);
ws.onmessage = (ev) => {
if (ev.data == "reload") {
window.location.reload();
}
var ws = new WebSocket(url);
ws.onmessage = (ev) => {
console.log("Received message: ", ev, ev.data);
if (ev.data == "reload") {
window.location.reload();
}
};
ws.onclose = reload_upon_connect;
})()
+1 -1
View File
@@ -64,7 +64,7 @@ impl ExecWithRustFlagsSetter for subprocess::Exec {
/// Build client (WASM).
/// Note: `rust_flags` argument is only used for the fullstack platform.
pub fn build(
pub fn build_web(
config: &CrateConfig,
skip_assets: bool,
rust_flags: Option<String>,
+2 -2
View File
@@ -57,7 +57,7 @@ impl Build {
let build_result = match platform {
Platform::Web => {
// `rust_flags` are used by fullstack's client build.
crate::builder::build(&crate_config, self.build.skip_assets, rust_flags)?
crate::builder::build_web(&crate_config, self.build.skip_assets, rust_flags)?
}
Platform::Desktop => {
// Since desktop platform doesn't use `rust_flags`, this
@@ -80,7 +80,7 @@ impl Build {
}
None => web_config.features = Some(vec![web_feature]),
};
crate::builder::build(
crate::builder::build_web(
&web_config,
self.build.skip_assets,
Some(client_rust_flags),
+1 -1
View File
@@ -138,7 +138,7 @@ async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()
.unwrap()
.map
.values()
.filter_map(|(_, template_slot)| *template_slot)
.flat_map(|v| v.templates.values().copied())
.collect()
};
for template in templates {
+72 -14
View File
@@ -6,6 +6,7 @@ use dioxus_core::Template;
use dioxus_hot_reload::HotReloadMsg;
use dioxus_html::HtmlCtx;
use dioxus_rsx::hot_reload::*;
use fs_extra::{dir::CopyOptions, file};
use notify::{RecommendedWatcher, Watcher};
use std::{
path::PathBuf,
@@ -157,17 +158,9 @@ fn hotreload_files(
for path in &event.paths {
// for various assets that might be linked in, we just try to hotreloading them forcefully
// That is, unless they appear in an include! macro, in which case we need to a full rebuild....
// if this is not a rust file, rebuild the whole project
let path_extension = path.extension().and_then(|p| p.to_str());
if path_extension != Some("rs") {
*needs_full_rebuild = true;
if path_extension == Some("rs~") {
*needs_full_rebuild = false;
}
let Some(ext) = path.extension().and_then(|v| v.to_str()) else {
continue;
}
};
// Workaround for notify and vscode-like editor:
// when edit & save a file in vscode, there will be two notifications,
@@ -179,8 +172,67 @@ fn hotreload_files(
}
}
match ext {
// Attempt hot reload
"rs" => {}
// Anything with a .file is also ignored
_ if path.file_stem().is_none() || ext.ends_with("~") => {}
// Anything else is a maybe important file that needs to be rebuilt
_ => {
// If it happens to be a file in the asset directory, there's a chance we can hotreload it.
// Only css is currently supported for hotreload
if ext == "css" {
let asset_dir = config
.crate_dir
.join(&config.dioxus_config.application.asset_dir);
if path.starts_with(&asset_dir) {
let local_path: PathBuf = path
.file_name()
.unwrap()
.to_str()
.unwrap()
.to_string()
.parse()
.unwrap();
println!(
"maybe tracking asset: {:?}, {:#?}",
local_path,
rsx_file_map.tracked_assets()
);
if let Some(f) = rsx_file_map.is_tracking_asset(&local_path) {
println!(
"Hot reloading asset - it's tracked by the rsx!: {:?}",
local_path
);
// copy the asset over tothe output directory
let output_dir = config.out_dir();
fs_extra::copy_items(
&[path],
output_dir,
&CopyOptions::new().overwrite(true),
)
.unwrap();
messages.push(HotReloadMsg::UpdateAsset(local_path));
continue;
}
}
}
*needs_full_rebuild = true;
}
};
match rsx_file_map.update_rsx(path, &config.crate_dir) {
Ok(UpdateResult::UpdatedRsx(msgs)) => {
println!("Updated: {:?}", msgs);
messages.extend(
msgs.into_iter()
.map(|msg| HotReloadMsg::UpdateTemplate(msg)),
@@ -196,6 +248,8 @@ fn hotreload_files(
}
}
// If full rebuild, extend the file map with the new file map
// This will wipe away any previous cached changed templates
if *needs_full_rebuild {
// Reset the file map to the new state of the project
let FileMapBuildResult {
@@ -208,10 +262,14 @@ fn hotreload_files(
}
*rsx_file_map = new_file_map;
} else {
for msg in messages {
let _ = hot_reload.messages.send(msg);
}
return;
}
println!("Hot reloading: {:?}", messages);
for msg in messages {
let _ = hot_reload.messages.send(msg);
}
}
+31 -7
View File
@@ -8,6 +8,7 @@ use axum::{
Extension,
};
use dioxus_hot_reload::HotReloadMsg;
use futures_util::{pin_mut, FutureExt};
pub async fn hot_reload_handler(
ws: WebSocketUpgrade,
@@ -34,9 +35,11 @@ async fn hotreload_loop(mut socket: WebSocket, state: HotReloadState) -> anyhow:
.unwrap()
.map
.values()
.filter_map(|(_, template_slot)| *template_slot)
.flat_map(|v| v.templates.values().copied())
.collect::<Vec<_>>();
println!("previously changed: {:?}", templates);
for template in templates {
socket
.send(Message::Text(serde_json::to_string(&template).unwrap()))
@@ -46,16 +49,37 @@ async fn hotreload_loop(mut socket: WebSocket, state: HotReloadState) -> anyhow:
let mut rx = state.messages.subscribe();
loop {
if let Ok(msg) = rx.recv().await {
let msg = match msg {
let msg = {
// Poll both the receiver and the socket
//
// This shuts us down if the connection is closed.
let mut _socket = socket.recv().fuse();
let mut _rx = rx.recv().fuse();
pin_mut!(_socket, _rx);
let msg = futures_util::select! {
msg = _rx => msg,
_ = _socket => break,
};
let Ok(msg) = msg else { break };
println!("msg: {:?}", msg);
match msg {
HotReloadMsg::UpdateTemplate(template) => {
Message::Text(serde_json::to_string(&template).unwrap())
}
HotReloadMsg::UpdateAsset(_) => todo!(),
HotReloadMsg::UpdateAsset(asset) => {
Message::Text(format!("asset: {}", asset.display()))
}
HotReloadMsg::Shutdown => todo!(),
};
}
};
socket.send(msg).await?;
}
socket.send(msg).await?;
}
Ok(())
}
+2 -2
View File
@@ -52,7 +52,7 @@ pub async fn serve(
// Since web platform doesn't use `rust_flags`, this argument is explicitly
// set to `None`.
let first_build_result = crate::builder::build(&config, skip_assets, None)?;
let first_build_result = crate::builder::build_web(&config, skip_assets, None)?;
// generate dev-index page
Serve::regen_dev_page(&config, first_build_result.assets.as_ref())?;
@@ -173,7 +173,7 @@ fn build(
) -> Result<BuildResult> {
// Since web platform doesn't use `rust_flags`, this argument is explicitly
// set to `None`.
let result = std::panic::catch_unwind(|| builder::build(config, skip_assets, None))
let result = std::panic::catch_unwind(|| builder::build_web(config, skip_assets, None))
.map_err(|e| anyhow::anyhow!("Build failed: {e:?}"))?;
// change the websocket reload state to true;
+2
View File
@@ -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);
}
+208 -211
View File
@@ -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 {
+28 -13
View File
@@ -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)
}
}
+1
View File
@@ -74,6 +74,7 @@ impl CallBody {
roots: &self.roots,
location: None,
};
renderer.update_template::<Ctx>(template, location)
}
+12 -20
View File
@@ -24,7 +24,11 @@ wasm-bindgen-futures = "0.4.29"
tracing = { workspace = true }
rustc-hash = { workspace = true }
console_error_panic_hook = { version = "0.1.7", optional = true }
futures-util = { workspace = true, features = ["std", "async-await", "async-await-macro"] }
futures-util = { workspace = true, features = [
"std",
"async-await",
"async-await-macro",
] }
futures-channel = { workspace = true }
serde_json = { version = "1.0" }
serde = { version = "1.0" }
@@ -43,35 +47,23 @@ features = [
"Text",
"Window",
"DataTransfer",
"console"
"console",
"NodeList",
]
[features]
default = ["panic_hook", "mounted", "file_engine", "hot_reload", "eval"]
panic_hook = ["console_error_panic_hook"]
hydrate = [
"web-sys/Comment",
]
mounted = [
"web-sys/Element",
"dioxus-html/mounted"
]
hydrate = ["web-sys/Comment"]
mounted = ["web-sys/Element", "dioxus-html/mounted"]
file_engine = [
"web-sys/File",
"web-sys/FileList",
"web-sys/FileReader",
"async-trait"
]
hot_reload = [
"web-sys/MessageEvent",
"web-sys/WebSocket",
"web-sys/Location",
]
eval = [
"dioxus-html/eval",
"serde-wasm-bindgen",
"async-trait"
"async-trait",
]
hot_reload = ["web-sys/MessageEvent", "web-sys/WebSocket", "web-sys/Location"]
eval = ["dioxus-html/eval", "serde-wasm-bindgen", "async-trait"]
[dev-dependencies]
dioxus = { workspace = true }
+31 -5
View File
@@ -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)>);