diff --git a/Cargo.toml b/Cargo.toml index 7d8cc6871..ebfe288c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,8 @@ members = [ "packages/native-core-macro", "packages/rsx-rosetta", "packages/signals", + "packages/hot-reload", + "packages/hot-reload-macro", "docs/guide", ] @@ -43,6 +45,7 @@ publish = false dioxus = { path = "./packages/dioxus" } dioxus-desktop = { path = "./packages/desktop", features = ["transparent"] } dioxus-ssr = { path = "./packages/ssr" } +dioxus-hot-reload = { path = "./packages/hot-reload" } dioxus-router = { path = "./packages/router" } dioxus-signals = { path = "./packages/signals" } fermi = { path = "./packages/fermi" } diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 5f1ea27d7..16cc622c6 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -33,7 +33,7 @@ webbrowser = "0.8.0" infer = "0.11.0" dunce = "1.0.2" -interprocess = { version = "1.1.1", optional = true } +interprocess = { version = "1.2.1", optional = true } futures-util = "0.3.25" [target.'cfg(target_os = "ios")'.dependencies] diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index d3b9216b0..720223e6c 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -8,6 +8,7 @@ use crate::events::IpcMessage; use crate::Config; use crate::WebviewHandler; use dioxus_core::ScopeState; +use dioxus_core::Template; use dioxus_core::VirtualDom; use serde_json::Value; use wry::application::event_loop::EventLoopProxy; @@ -262,6 +263,8 @@ pub enum EventData { Ipc(IpcMessage), + TemplateUpdated(Template<'static>), + NewWindow, CloseWindow, diff --git a/packages/desktop/src/hot_reload.rs b/packages/desktop/src/hot_reload.rs index e1383390d..20907f59d 100644 --- a/packages/desktop/src/hot_reload.rs +++ b/packages/desktop/src/hot_reload.rs @@ -2,43 +2,29 @@ use dioxus_core::Template; -use interprocess::local_socket::{LocalSocketListener, LocalSocketStream}; +use interprocess::local_socket::LocalSocketStream; use std::io::{BufRead, BufReader}; -use std::time::Duration; -use std::{sync::Arc, sync::Mutex}; +use wry::application::{event_loop::EventLoopProxy, window::WindowId}; -fn handle_error(connection: std::io::Result) -> Option { - connection - .map_err(|error| eprintln!("Incoming connection failed: {}", error)) - .ok() -} +use crate::desktop_context::{EventData, UserWindowEvent}; -pub(crate) fn init(proxy: futures_channel::mpsc::UnboundedSender>) { - let latest_in_connection: Arc>>> = - Arc::new(Mutex::new(None)); - - let latest_in_connection_handle = latest_in_connection.clone(); - - // connect to processes for incoming data +pub(crate) fn init(proxy: EventLoopProxy) { std::thread::spawn(move || { let temp_file = std::env::temp_dir().join("@dioxusin"); - - if let Ok(listener) = LocalSocketListener::bind(temp_file) { - for conn in listener.incoming().filter_map(handle_error) { - *latest_in_connection_handle.lock().unwrap() = Some(BufReader::new(conn)); - } - } - }); - - std::thread::spawn(move || { - loop { - if let Some(conn) = &mut *latest_in_connection.lock().unwrap() { + if let Ok(socket) = LocalSocketStream::connect(temp_file.as_path()) { + let mut buf_reader = BufReader::new(socket); + loop { let mut buf = String::new(); - match conn.read_line(&mut buf) { + match buf_reader.read_line(&mut buf) { Ok(_) => { - let msg: Template<'static> = + let template: Template<'static> = serde_json::from_str(Box::leak(buf.into_boxed_str())).unwrap(); - proxy.unbounded_send(msg).unwrap(); + proxy + .send_event(UserWindowEvent( + EventData::TemplateUpdated(template), + unsafe { WindowId::dummy() }, + )) + .unwrap(); } Err(err) => { if err.kind() != std::io::ErrorKind::WouldBlock { @@ -47,8 +33,6 @@ pub(crate) fn init(proxy: futures_channel::mpsc::UnboundedSender(root: Component

, props: P, cfg: Config) let proxy = event_loop.create_proxy(); + // Intialize hot reloading if it is enabled + #[cfg(all(feature = "hot-reload", debug_assertions))] + hot_reload::init(proxy.clone()); + // We start the tokio runtime *on this thread* // Any future we poll later will use this runtime to spawn tasks and for IO let rt = tokio::runtime::Builder::new_multi_thread() @@ -168,6 +172,14 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) } Event::UserEvent(event) => match event.0 { + EventData::TemplateUpdated(template) => { + for webview in webviews.values_mut() { + webview.dom.replace_template(template); + + poll_vdom(webview); + } + } + EventData::CloseWindow => { webviews.remove(&event.1); diff --git a/packages/hot-reload-macro/Cargo.toml b/packages/hot-reload-macro/Cargo.toml new file mode 100644 index 000000000..009c6501a --- /dev/null +++ b/packages/hot-reload-macro/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "dioxus-hot-reload-macro" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +syn = { version = "1.0.107", features = ["full"] } +proc-macro2 = { version = "1.0" } + +[lib] +proc-macro = true \ No newline at end of file diff --git a/packages/hot-reload-macro/src/lib.rs b/packages/hot-reload-macro/src/lib.rs new file mode 100644 index 000000000..211be00cf --- /dev/null +++ b/packages/hot-reload-macro/src/lib.rs @@ -0,0 +1,7 @@ +use proc_macro::TokenStream; +use syn::__private::quote::quote; + +#[proc_macro] +pub fn hot_reload(_: TokenStream) -> TokenStream { + quote!(dioxus_hot_reload::init(core::env!("CARGO_MANIFEST_DIR"))).into() +} diff --git a/packages/hot-reload/Cargo.toml b/packages/hot-reload/Cargo.toml new file mode 100644 index 000000000..5c9f34826 --- /dev/null +++ b/packages/hot-reload/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "dioxus-hot-reload" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +dioxus-hot-reload-macro = { path = "../hot-reload-macro" } +dioxus-rsx = { path = "../rsx" } +dioxus-core = { path = "../core", features = ["serialize"] } +dioxus-html = { path = "../html", features = ["hot-reload-context"] } + +interprocess = { version = "1.2.1" } +notify = "5.0.0" +chrono = "0.4.23" +serde_json = "1.0.91" diff --git a/packages/hot-reload/src/lib.rs b/packages/hot-reload/src/lib.rs new file mode 100644 index 000000000..4918b0c03 --- /dev/null +++ b/packages/hot-reload/src/lib.rs @@ -0,0 +1,84 @@ +use std::{ + io::Write, + path::PathBuf, + str::FromStr, + sync::{Arc, Mutex}, +}; + +pub use dioxus_hot_reload_macro::hot_reload; +use dioxus_html::HtmlCtx; +use dioxus_rsx::hot_reload::{FileMap, UpdateResult}; +use interprocess::local_socket::LocalSocketListener; +use notify::{RecommendedWatcher, RecursiveMode, Watcher}; + +pub fn init(path: &'static str) { + if let Ok(crate_dir) = PathBuf::from_str(path) { + let temp_file = std::env::temp_dir().join("@dioxusin"); + let channels = Arc::new(Mutex::new(Vec::new())); + if let Ok(local_socket_stream) = LocalSocketListener::bind(temp_file.as_path()) { + // listen for connections + std::thread::spawn({ + let channels = channels.clone(); + move || { + for connection in local_socket_stream.incoming() { + if let Ok(connection) = connection { + channels.lock().unwrap().push(connection); + println!("Connected to hot reloading 🚀"); + } + } + } + }); + + // watch for changes + std::thread::spawn(move || { + let mut last_update_time = chrono::Local::now().timestamp(); + let mut file_map = FileMap::::new(crate_dir.clone()); + + let (tx, rx) = std::sync::mpsc::channel(); + + let mut watcher = RecommendedWatcher::new(tx, notify::Config::default()).unwrap(); + + let mut examples_path = crate_dir.clone(); + examples_path.push("examples"); + let _ = watcher.watch(&examples_path, RecursiveMode::Recursive); + let mut src_path = crate_dir.clone(); + src_path.push("src"); + let _ = watcher.watch(&src_path, RecursiveMode::Recursive); + + for evt in rx { + // Give time for the change to take effect before reading the file + std::thread::sleep(std::time::Duration::from_millis(100)); + if chrono::Local::now().timestamp() > last_update_time { + if let Ok(evt) = evt { + let mut channels = channels.lock().unwrap(); + for path in &evt.paths { + // skip non rust files + if path.extension().and_then(|p| p.to_str()) != Some("rs") { + continue; + } + + // find changes to the rsx in the file + match file_map.update_rsx(&path, crate_dir.as_path()) { + UpdateResult::UpdatedRsx(msgs) => { + for msg in msgs { + for channel in channels.iter_mut() { + let msg = serde_json::to_string(&msg).unwrap(); + channel.write_all(msg.as_bytes()).unwrap(); + channel.write_all(&[b'\n']).unwrap(); + } + } + } + UpdateResult::NeedsRebuild => { + println!("Rebuild needed... shutting down hot reloading"); + return; + } + } + } + } + last_update_time = chrono::Local::now().timestamp(); + } + } + }); + } + } +}