diff --git a/Cargo.toml b/Cargo.toml index 7d8cc6871..e32940e18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ members = [ "packages/native-core-macro", "packages/rsx-rosetta", "packages/signals", + "packages/hot-reload", "docs/guide", ] diff --git a/docs/guide/src/en/SUMMARY.md b/docs/guide/src/en/SUMMARY.md index 83840fe90..f9f31ed52 100644 --- a/docs/guide/src/en/SUMMARY.md +++ b/docs/guide/src/en/SUMMARY.md @@ -5,11 +5,11 @@ - [Getting Started](getting_started/index.md) - [Desktop](getting_started/desktop.md) - [Web](getting_started/web.md) - - [Hot Reload](getting_started/hot_reload.md) - [Server-Side Rendering](getting_started/ssr.md) - [Liveview](getting_started/liveview.md) - [Terminal UI](getting_started/tui.md) - [Mobile](getting_started/mobile.md) + - [Hot Reloading](getting_started/hot_reload.md) - [Describing the UI](describing_ui/index.md) - [Special Attributes](describing_ui/special_attributes.md) - [Components](describing_ui/components.md) diff --git a/docs/guide/src/en/getting_started/hot_reload.md b/docs/guide/src/en/getting_started/hot_reload.md index 823baa179..599ce57da 100644 --- a/docs/guide/src/en/getting_started/hot_reload.md +++ b/docs/guide/src/en/getting_started/hot_reload.md @@ -2,21 +2,48 @@ 1. Hot reloading allows much faster iteration times inside of rsx calls by interpreting them and streaming the edits. 2. It is useful when changing the styling/layout of a program, but will not help with changing the logic of a program. -3. Currently the cli only implements hot reloading for the web renderer. +3. Currently the cli only implements hot reloading for the web renderer. For TUI, desktop, and LiveView you can use the hot reload macro instead. -# Setup +# Web +For the web renderer, you can use the dioxus cli to serve your application with hot reloading enabled. + +## Setup Install [dioxus-cli](https://github.com/DioxusLabs/cli). Hot reloading is automatically enabled when using the web renderer on debug builds. -# Usage -1. run: -``` +## Usage +1. Run: +```bash dioxus serve --hot-reload ``` -2. change some code within a rsx macro -3. open your localhost in a browser -4. save and watch the style change without recompiling +2. Change some code within a rsx or render macro +3. Open your localhost in a browser +4. Save and watch the style change without recompiling + +# Desktop/Liveview/TUI +For desktop, LiveView, and tui, you can place the hot reload macro at the top of your main function to enable hot reloading. +Hot reloading is automatically enabled on debug builds. + +For more information about hot reloading on native platforms and configuration options see the [dioxus-hot-reload](https://crates.io/crates/dioxus-hot-reload) crate. + +## Setup +Add the following to your main function: + +```rust +fn main() { + hot_reload_init!(); + // launch your application +} +``` + +## Usage +1. Run: +```bash +cargo run +``` +2. Change some code within a rsx or render macro +3. Save and watch the style change without recompiling # Limitations -1. The interpreter can only use expressions that existed on the last full recompile. If you introduce a new variable or expression to the rsx call, it will trigger a full recompile to capture the expression. -2. Components and Iterators can contain arbitrary rust code and will trigger a full recompile when changed. \ No newline at end of file +1. The interpreter can only use expressions that existed on the last full recompile. If you introduce a new variable or expression to the rsx call, it will require a full recompile to capture the expression. +2. Components, Iterators, and some attributes can contain arbitrary rust code and will trigger a full recompile when changed. diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 9b008ef55..7645e4de5 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -15,6 +15,7 @@ keywords = ["dom", "ui", "gui", "react"] dioxus-core = { path = "../core", version = "^0.3.0", features = ["serialize"] } dioxus-html = { path = "../html", features = ["serialize"], version = "^0.3.0" } dioxus-interpreter-js = { path = "../interpreter", version = "^0.3.0" } +dioxus-hot-reload = { path = "../hot-reload", optional = true } serde = "1.0.136" serde_json = "1.0.79" @@ -34,7 +35,6 @@ infer = "0.11.0" dunce = "1.0.2" slab = "0.4" -interprocess = { version = "1.1.1", optional = true } futures-util = "0.3.25" [target.'cfg(target_os = "ios")'.dependencies] @@ -50,7 +50,7 @@ tokio_runtime = ["tokio"] fullscreen = ["wry/fullscreen"] transparent = ["wry/transparent"] tray = ["wry/tray"] -hot-reload = ["interprocess"] +hot-reload = ["dioxus-hot-reload"] [dev-dependencies] dioxus-core-macro = { path = "../core-macro" } diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index c90e97354..1cebc3a28 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -9,6 +9,7 @@ use crate::Config; use crate::WebviewHandler; use dioxus_core::ScopeState; use dioxus_core::VirtualDom; +use dioxus_hot_reload::HotReloadMsg; use serde_json::Value; use slab::Slab; use wry::application::event::Event; @@ -285,6 +286,8 @@ pub enum EventData { Ipc(IpcMessage), + HotReloadEvent(HotReloadMsg), + NewWindow, CloseWindow, diff --git a/packages/desktop/src/hot_reload.rs b/packages/desktop/src/hot_reload.rs deleted file mode 100644 index e1383390d..000000000 --- a/packages/desktop/src/hot_reload.rs +++ /dev/null @@ -1,54 +0,0 @@ -#![allow(dead_code)] - -use dioxus_core::Template; - -use interprocess::local_socket::{LocalSocketListener, LocalSocketStream}; -use std::io::{BufRead, BufReader}; -use std::time::Duration; -use std::{sync::Arc, sync::Mutex}; - -fn handle_error(connection: std::io::Result) -> Option { - connection - .map_err(|error| eprintln!("Incoming connection failed: {}", error)) - .ok() -} - -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 - 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() { - let mut buf = String::new(); - match conn.read_line(&mut buf) { - Ok(_) => { - let msg: Template<'static> = - serde_json::from_str(Box::leak(buf.into_boxed_str())).unwrap(); - proxy.unbounded_send(msg).unwrap(); - } - Err(err) => { - if err.kind() != std::io::ErrorKind::WouldBlock { - break; - } - } - } - } - // give the error handler time to take the mutex - std::thread::sleep(Duration::from_millis(100)); - } - }); -} diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 9d3130fac..824bd81f5 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -12,9 +12,6 @@ mod protocol; mod waker; mod webview; -#[cfg(all(feature = "hot-reload", debug_assertions))] -mod hot_reload; - pub use cfg::Config; pub use desktop_context::{ use_window, use_wry_event_handler, DesktopContext, WryEventHandler, WryEventHandlerId, @@ -111,6 +108,18 @@ pub fn launch_with_props(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))] + { + let proxy = proxy.clone(); + dioxus_hot_reload::connect(move |template| { + let _ = proxy.send_event(UserWindowEvent( + EventData::HotReloadEvent(template), + unsafe { WindowId::dummy() }, + )); + }); + } + // 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() @@ -176,6 +185,19 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) } Event::UserEvent(event) => match event.0 { + EventData::HotReloadEvent(msg) => match msg { + dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => { + for webview in webviews.values_mut() { + webview.dom.replace_template(template); + + poll_vdom(webview); + } + } + dioxus_hot_reload::HotReloadMsg::Shutdown => { + *control_flow = ControlFlow::Exit; + } + }, + EventData::CloseWindow => { webviews.remove(&event.1); diff --git a/packages/dioxus/Cargo.toml b/packages/dioxus/Cargo.toml index ceab90a1b..afe0ea0c3 100644 --- a/packages/dioxus/Cargo.toml +++ b/packages/dioxus/Cargo.toml @@ -18,11 +18,15 @@ dioxus-core-macro = { path = "../core-macro", version = "^0.3.0", optional = tru dioxus-hooks = { path = "../hooks", version = "^0.3.0", optional = true } dioxus-rsx = { path = "../rsx", version = "0.0.2", optional = true } +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +dioxus-hot-reload = { path = "../hot-reload", version = "0.1.0", optional = true } + [features] -default = ["macro", "hooks", "html"] +default = ["macro", "hooks", "html", "hot-reload"] macro = ["dioxus-core-macro", "dioxus-rsx"] html = ["dioxus-html"] hooks = ["dioxus-hooks"] +hot-reload = ["dioxus-hot-reload"] [dev-dependencies] diff --git a/packages/dioxus/src/lib.rs b/packages/dioxus/src/lib.rs index fed3d2bd5..5d1371f27 100644 --- a/packages/dioxus/src/lib.rs +++ b/packages/dioxus/src/lib.rs @@ -31,4 +31,7 @@ pub mod prelude { #[cfg(feature = "html")] pub use dioxus_elements::{prelude::*, GlobalAttributes, SvgAttributes}; + + #[cfg(all(not(target_arch = "wasm32"), feature = "hot-reload"))] + pub use dioxus_hot_reload::{self, hot_reload_init}; } diff --git a/packages/hot-reload/Cargo.toml b/packages/hot-reload/Cargo.toml new file mode 100644 index 000000000..99d5cb00d --- /dev/null +++ b/packages/hot-reload/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "dioxus-hot-reload" +version = "0.1.0" +edition = "2021" +license = "MIT/Apache-2.0" +repository = "https://github.com/DioxusLabs/dioxus/" +homepage = "https://dioxuslabs.com" +description = "Hot reloading utilites for Dioxus" +documentation = "https://dioxuslabs.com" +keywords = ["dom", "ui", "gui", "react", "hot-reloading", "watch"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +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" +serde = { version = "1", features = ["derive"] } +execute = "0.2.11" +once_cell = "1.17.0" +ignore = "0.4.19" \ No newline at end of file diff --git a/packages/hot-reload/README.md b/packages/hot-reload/README.md new file mode 100644 index 000000000..126c42103 --- /dev/null +++ b/packages/hot-reload/README.md @@ -0,0 +1,170 @@ +# `dioxus-hot-reload`: Hot Reloading Utilites for Dioxus + + +[![Crates.io][crates-badge]][crates-url] +[![MIT licensed][mit-badge]][mit-url] +[![Build Status][actions-badge]][actions-url] +[![Discord chat][discord-badge]][discord-url] + +[crates-badge]: https://img.shields.io/crates/v/dioxus-hot-reload.svg +[crates-url]: https://crates.io/crates/dioxus-hot-reload + +[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg +[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE + +[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg +[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster + +[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square +[discord-url]: https://discord.gg/XgGxMSkvUM + +[Website](https://dioxuslabs.com) | +[Guides](https://dioxuslabs.com/guide/) | +[API Docs](https://docs.rs/dioxus-hot-reload/latest/dioxus_hot_reload) | +[Chat](https://discord.gg/XgGxMSkvUM) + + +## Overview + +Dioxus supports hot reloading for static parts of rsx macros. This enables changing the styling of your application without recompiling the rust code. This is useful for rapid iteration on the styling of your application. + + +Hot reloading could update the following change without recompiling: +```rust +rsx! { + div { + "Count: {count}", + } +} +``` +=> +```rust +rsx! { + div { + color: "red", + font_size: "2em", + "Count: {count}", + } +} +``` + +But it could not update the following change: +```rust +rsx! { + div { + "Count: {count}", + } +} +``` +=> +```rust +rsx! { + div { + "Count: {count*2}", + onclick: |_| println!("clicked"), + } +} +``` + +## Usage + +> This crate implements hot reloading for native compilation targets not WASM. For hot relaoding with the web renderer, see the [dioxus-cli](https://github.com/DioxusLabs/cli) project. + +Add this to the top of your main function on any renderer that supports hot reloading to start the hot reloading server: + +```rust +fn main(){ + hot_reload_init!(); + // launch your application +} +``` + +By default the dev server watches on the root of the crate the macro is called in and ignores changes in the `/target` directory and any directories set in the `.gitignore` file in the root directory. To watch on custom paths pass call the `with_paths` function on the config builder: + +```rust +fn main(){ + hot_reload_init!(Config::new().with_paths(&["src", "examples", "assets"])); + // launch your application +} +``` + +By default the hot reloading server will output some logs in the console, to disable these logs call the `with_logging` function on the config builder: + +```rust +fn main(){ + hot_reload_init!(Config::new().with_logging(false)); + // launch your application +} +``` + +To rebuild the application when the logic changes, you can use the `with_rebuild_command` function on the config builder. This command will be called when hot reloading fails to quickly update the rsx: + +```rust +fn main(){ + hot_reload_init!(Config::new().with_rebuild_command("cargo run")); + // launch your application +} +``` + +If you are using a namespace other than html, you can implement the [HotReloadingContext](https://docs.rs/dioxus-rsx/latest/dioxus_rsx/trait.HotReloadingContext.html) trait to provide a mapping between the rust names of your elements/attributes and the resulting strings. + +You can then provide the Context to the builder to make hot reloading work with your custom namespace: + +```rust +fn main(){ + // Note: Use default instead of new if you are using a custom namespace + hot_reload_init!(Config::::default()); + // launch your application +} +``` + +## Implementing Hot Reloading for a Custom Renderer + +To add hot reloading support to your custom renderer you can use the connect function. This will connect to the dev server you just need to provide a way to transfer `Template`s to the `VirtualDom`. Once you implement this your users can use the hot_reload_init function just like any other render. + +```rust +async fn launch(app: Component) { + let mut vdom = VirtualDom::new(app); + // ... + + let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); + dioxus_hot_reload::connect(move |msg| { + let _ = tx.send(msg); + }); + + loop { + tokio::select! { + Some(msg) = rx.recv() => { + match msg{ + HotReloadMsg::Shutdown => { + // ... shutdown the application + } + HotReloadMsg::UpdateTemplate(template) => { + // update the template in the virtual dom + vdom.replace_template(template); + } + } + } + _ = vdom.wait_for_work() => { + // ... + } + } + let mutations = vdom.render_immediate(); + // apply the mutations to the dom + } +} +``` + +## Contributing + +- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues). +- Join the discord and ask questions! + +## License +This project is licensed under the [MIT license]. + +[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in Dioxus by you shall be licensed as MIT without any additional +terms or conditions. diff --git a/packages/hot-reload/src/lib.rs b/packages/hot-reload/src/lib.rs new file mode 100644 index 000000000..739cbfc55 --- /dev/null +++ b/packages/hot-reload/src/lib.rs @@ -0,0 +1,360 @@ +use std::{ + io::{BufRead, BufReader, Write}, + path::PathBuf, + str::FromStr, + sync::{Arc, Mutex}, +}; + +use dioxus_core::Template; +use dioxus_rsx::{ + hot_reload::{FileMap, UpdateResult}, + HotReloadingContext, +}; +use interprocess::local_socket::{LocalSocketListener, LocalSocketStream}; +use notify::{RecommendedWatcher, RecursiveMode, Watcher}; + +#[cfg(debug_assertions)] +pub use dioxus_html::HtmlCtx; +use serde::{Deserialize, Serialize}; + +/// A message the hot reloading server sends to the client +#[derive(Debug, Serialize, Deserialize, Clone, Copy)] +pub enum HotReloadMsg { + /// A template has been updated + #[serde(borrow = "'static")] + UpdateTemplate(Template<'static>), + /// The program needs to be recompiled, and the client should shut down + Shutdown, +} + +pub struct Config { + root_path: &'static str, + listening_paths: &'static [&'static str], + excluded_paths: &'static [&'static str], + log: bool, + rebuild_with: Option bool + Send + 'static>>, + phantom: std::marker::PhantomData, +} + +impl Default for Config { + fn default() -> Self { + Self { + root_path: "", + listening_paths: &[""], + excluded_paths: &["./target"], + log: true, + rebuild_with: None, + phantom: std::marker::PhantomData, + } + } +} + +impl Config { + pub const fn new() -> Self { + Self { + root_path: "", + listening_paths: &[""], + excluded_paths: &["./target"], + log: true, + rebuild_with: None, + phantom: std::marker::PhantomData, + } + } +} + +impl Config { + /// Set the root path of the project (where the Cargo.toml file is). This is automatically set by the [`hot_reload_init`] macro. + pub fn root(self, path: &'static str) -> Self { + Self { + root_path: path, + ..self + } + } + + /// Set whether to enable logs + pub fn with_logging(self, log: bool) -> Self { + Self { log, ..self } + } + + /// Set the command to run to rebuild the project + /// + /// For example to restart the application after a change is made, you could use `cargo run` + pub fn with_rebuild_command(self, rebuild_command: &'static str) -> Self { + self.with_rebuild_callback(move || { + execute::shell(rebuild_command) + .spawn() + .expect("Failed to spawn the rebuild command"); + true + }) + } + + /// Set a callback to run to when the project needs to be rebuilt and returns if the server should shut down + /// + /// For example a CLI application could rebuild the application when a change is made + pub fn with_rebuild_callback( + self, + rebuild_callback: impl FnMut() -> bool + Send + 'static, + ) -> Self { + Self { + rebuild_with: Some(Box::new(rebuild_callback)), + ..self + } + } + + /// Set the paths to listen for changes in to trigger hot reloading. If this is a directory it will listen for changes in all files in that directory recursively. + pub fn with_paths(self, paths: &'static [&'static str]) -> Self { + Self { + listening_paths: paths, + ..self + } + } + + /// Sets paths to ignore changes on. This will override any paths set in the [`Config::with_paths`] method in the case of conflicts. + pub fn excluded_paths(self, paths: &'static [&'static str]) -> Self { + Self { + excluded_paths: paths, + ..self + } + } +} + +/// Initialize the hot reloading listener +pub fn init(cfg: Config) { + let Config { + root_path, + listening_paths, + log, + mut rebuild_with, + excluded_paths, + phantom: _, + } = cfg; + + if let Ok(crate_dir) = PathBuf::from_str(root_path) { + let temp_file = std::env::temp_dir().join("@dioxusin"); + let channels = Arc::new(Mutex::new(Vec::new())); + let file_map = Arc::new(Mutex::new(FileMap::::new(crate_dir.clone()))); + if let Ok(local_socket_stream) = LocalSocketListener::bind(temp_file.as_path()) { + 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 🚀"); + } + } + std::thread::sleep(std::time::Duration::from_millis(10)); + if *aborted.lock().unwrap() { + break; + } + } + } + }); + + // watch for changes + std::thread::spawn(move || { + // try to find the gitingore file + let gitignore_file_path = crate_dir.join(".gitignore"); + let (gitignore, _) = ignore::gitignore::Gitignore::new(gitignore_file_path); + + 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 excluded_paths = excluded_paths + .iter() + .map(|path| crate_dir.join(PathBuf::from(path))) + .collect::>(); + + 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 futher changes." + ); + } + true + } + }; + + for evt in rx { + if chrono::Local::now().timestamp() > 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::>(); + + // 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()) + { + 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); + } + } + } + } + UpdateResult::NeedsRebuild => { + drop(channels); + if rebuild() { + return; + } + break; + } + } + } + } + last_update_time = chrono::Local::now().timestamp(); + } + } + }); + } + } +} + +fn send_msg(msg: HotReloadMsg, channel: &mut impl Write) -> bool { + if let Ok(msg) = serde_json::to_string(&msg) { + if channel.write_all(msg.as_bytes()).is_err() { + return false; + } + if channel.write_all(&[b'\n']).is_err() { + return false; + } + true + } else { + false + } +} + +/// Connect to the hot reloading listener. The callback provided will be called every time a template change is detected +pub fn connect(mut f: impl FnMut(HotReloadMsg) + Send + 'static) { + std::thread::spawn(move || { + let temp_file = std::env::temp_dir().join("@dioxusin"); + if let Ok(socket) = LocalSocketStream::connect(temp_file.as_path()) { + let mut buf_reader = BufReader::new(socket); + loop { + let mut buf = String::new(); + match buf_reader.read_line(&mut buf) { + Ok(_) => { + let template: HotReloadMsg = + serde_json::from_str(Box::leak(buf.into_boxed_str())).unwrap(); + f(template); + } + Err(err) => { + if err.kind() != std::io::ErrorKind::WouldBlock { + break; + } + } + } + } + } + }); +} + +/// Start the hot reloading server with the current directory as the root +#[macro_export] +macro_rules! hot_reload_init { + () => { + #[cfg(debug_assertions)] + dioxus_hot_reload::init(dioxus_hot_reload::Config::new().root(env!("CARGO_MANIFEST_DIR"))); + }; + + ($cfg: expr) => { + #[cfg(debug_assertions)] + dioxus_hot_reload::init($cfg.root(env!("CARGO_MANIFEST_DIR"))); + }; +} diff --git a/packages/liveview/Cargo.toml b/packages/liveview/Cargo.toml index 2ea5f8cd0..fa097be5a 100644 --- a/packages/liveview/Cargo.toml +++ b/packages/liveview/Cargo.toml @@ -26,6 +26,7 @@ serde_json = "1.0.91" dioxus-html = { path = "../html", features = ["serialize"], version = "^0.3.0" } dioxus-core = { path = "../core", features = ["serialize"], version = "^0.3.0" } dioxus-interpreter-js = { path = "../interpreter", version = "0.3.0" } +dioxus-hot-reload = { path = "../hot-reload", optional = true } # warp warp = { version = "0.3.3", optional = true } @@ -51,8 +52,9 @@ salvo = { version = "0.37.7", features = ["affix", "ws"] } tower = "0.4.13" [features] -default = [] +default = ["hot-reload"] # actix = ["actix-files", "actix-web", "actix-ws"] +hot-reload = ["dioxus-hot-reload"] [[example]] name = "axum" diff --git a/packages/liveview/src/pool.rs b/packages/liveview/src/pool.rs index a20ad029e..b3ed6bef5 100644 --- a/packages/liveview/src/pool.rs +++ b/packages/liveview/src/pool.rs @@ -103,6 +103,15 @@ pub async fn run( where T: Send + 'static, { + #[cfg(all(feature = "hot-reload", debug_assertions))] + let mut hot_reload_rx = { + let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); + dioxus_hot_reload::connect(move |template| { + let _ = tx.send(template); + }); + rx + }; + let mut vdom = VirtualDom::new_with_props(app, props); // todo: use an efficient binary packed format for this @@ -122,6 +131,11 @@ where } loop { + #[cfg(all(feature = "hot-reload", debug_assertions))] + let hot_reload_wait = hot_reload_rx.recv(); + #[cfg(not(all(feature = "hot-reload", debug_assertions)))] + let hot_reload_wait = std::future::pending(); + tokio::select! { // poll any futures or suspense _ = vdom.wait_for_work() => {} @@ -142,6 +156,19 @@ where None => return Ok(()), } } + + msg = hot_reload_wait => { + if let Some(msg) = msg { + match msg{ + dioxus_hot_reload::HotReloadMsg::UpdateTemplate(new_template) => { + vdom.replace_template(new_template); + } + dioxus_hot_reload::HotReloadMsg::Shutdown => { + std::process::exit(0); + }, + } + } + } } let edits = vdom diff --git a/packages/tui/Cargo.toml b/packages/tui/Cargo.toml index 5341d40f4..bbf939d1a 100644 --- a/packages/tui/Cargo.toml +++ b/packages/tui/Cargo.toml @@ -14,10 +14,11 @@ license = "MIT/Apache-2.0" [dependencies] dioxus = { path = "../dioxus", version = "^0.3.0" } -dioxus-core = { path = "../core", version = "^0.3.0" } +dioxus-core = { path = "../core", version = "^0.3.0", features = ["serialize"] } dioxus-html = { path = "../html", version = "^0.3.0" } dioxus-native-core = { path = "../native-core", version = "^0.2.0" } dioxus-native-core-macro = { path = "../native-core-macro", version = "^0.2.0" } +dioxus-hot-reload = { path = "../hot-reload", optional = true } tui = "0.17.0" crossterm = "0.23.0" @@ -38,3 +39,7 @@ criterion = "0.3.5" [[bench]] name = "update" harness = false + +[features] +default = ["hot-reload"] +hot-reload = ["dioxus-hot-reload"] diff --git a/packages/tui/src/lib.rs b/packages/tui/src/lib.rs index 4c8d8e739..c7f1dcc79 100644 --- a/packages/tui/src/lib.rs +++ b/packages/tui/src/lib.rs @@ -22,6 +22,7 @@ use std::{ use std::{io, time::Duration}; use taffy::Taffy; pub use taffy::{geometry::Point, prelude::*}; +use tokio::{select, sync::mpsc::unbounded_channel}; use tui::{backend::CrosstermBackend, layout::Rect, Terminal}; mod config; @@ -144,6 +145,15 @@ fn render_vdom( .enable_all() .build()? .block_on(async { + #[cfg(all(feature = "hot-reload", debug_assertions))] + let mut hot_reload_rx = { + let (hot_reload_tx, hot_reload_rx) = + unbounded_channel::(); + dioxus_hot_reload::connect(move |msg| { + let _ = hot_reload_tx.send(msg); + }); + hot_reload_rx + }; let mut terminal = (!cfg.headless).then(|| { enable_raw_mode().unwrap(); let mut stdout = std::io::stdout(); @@ -223,16 +233,21 @@ fn render_vdom( } } - use futures::future::{select, Either}; + let mut hot_reload_msg = None; { let wait = vdom.wait_for_work(); + #[cfg(all(feature = "hot-reload", debug_assertions))] + let hot_reload_wait = hot_reload_rx.recv(); + #[cfg(not(all(feature = "hot-reload", debug_assertions)))] + let hot_reload_wait = std::future::pending(); + pin_mut!(wait); - match select(wait, event_reciever.next()).await { - Either::Left((_a, _b)) => { - // - } - Either::Right((evt, _o)) => { + select! { + _ = wait => { + + }, + evt = event_reciever.next() => { match evt.as_ref().unwrap() { InputEvent::UserInput(event) => match event { TermEvent::Key(key) => { @@ -252,6 +267,21 @@ fn render_vdom( if let InputEvent::UserInput(evt) = evt.unwrap() { register_event(evt); } + }, + Some(msg) = hot_reload_wait => { + hot_reload_msg = Some(msg); + } + } + } + + // if we have a new template, replace the old one + if let Some(msg) = hot_reload_msg { + match msg { + dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => { + vdom.replace_template(template); + } + dioxus_hot_reload::HotReloadMsg::Shutdown => { + break; } } }