mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-10 14:44:12 +00:00
Merge pull request #754 from Demonthos/desktop-hot-reload
Implement hot reloading for Desktop, TUI, and Liveview
This commit is contained in:
commit
1b597f43d1
16 changed files with 705 additions and 79 deletions
|
@ -20,6 +20,7 @@ members = [
|
|||
"packages/native-core-macro",
|
||||
"packages/rsx-rosetta",
|
||||
"packages/signals",
|
||||
"packages/hot-reload",
|
||||
"docs/guide",
|
||||
]
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
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.
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<LocalSocketStream>) -> Option<LocalSocketStream> {
|
||||
connection
|
||||
.map_err(|error| eprintln!("Incoming connection failed: {}", error))
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub(crate) fn init(proxy: futures_channel::mpsc::UnboundedSender<Template<'static>>) {
|
||||
let latest_in_connection: Arc<Mutex<Option<BufReader<LocalSocketStream>>>> =
|
||||
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));
|
||||
}
|
||||
});
|
||||
}
|
|
@ -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<P: 'static>(root: Component<P>, 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<P: 'static>(root: Component<P>, 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);
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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};
|
||||
}
|
||||
|
|
26
packages/hot-reload/Cargo.toml
Normal file
26
packages/hot-reload/Cargo.toml
Normal file
|
@ -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"
|
170
packages/hot-reload/README.md
Normal file
170
packages/hot-reload/README.md
Normal file
|
@ -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::<MyContext>::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.
|
360
packages/hot-reload/src/lib.rs
Normal file
360
packages/hot-reload/src/lib.rs
Normal file
|
@ -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<Ctx: HotReloadingContext = HtmlCtx> {
|
||||
root_path: &'static str,
|
||||
listening_paths: &'static [&'static str],
|
||||
excluded_paths: &'static [&'static str],
|
||||
log: bool,
|
||||
rebuild_with: Option<Box<dyn FnMut() -> bool + Send + 'static>>,
|
||||
phantom: std::marker::PhantomData<Ctx>,
|
||||
}
|
||||
|
||||
impl<Ctx: HotReloadingContext> Default for Config<Ctx> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
root_path: "",
|
||||
listening_paths: &[""],
|
||||
excluded_paths: &["./target"],
|
||||
log: true,
|
||||
rebuild_with: None,
|
||||
phantom: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config<HtmlCtx> {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
root_path: "",
|
||||
listening_paths: &[""],
|
||||
excluded_paths: &["./target"],
|
||||
log: true,
|
||||
rebuild_with: None,
|
||||
phantom: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Ctx: HotReloadingContext> Config<Ctx> {
|
||||
/// 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<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
|
||||
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::<Ctx>::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::<Vec<_>>();
|
||||
|
||||
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::<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())
|
||||
{
|
||||
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")));
|
||||
};
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -103,6 +103,15 @@ pub async fn run<T>(
|
|||
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
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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::HotReloadMsg>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue