dioxus_devtools/
lib.rs

1use dioxus_core::internal::HotReloadedTemplate;
2use dioxus_core::{ScopeId, VirtualDom};
3use dioxus_signals::{GlobalKey, Signal, WritableExt};
4
5pub use dioxus_devtools_types::*;
6pub use subsecond;
7use subsecond::PatchError;
8
9/// Applies template and literal changes to the VirtualDom
10///
11/// Assets need to be handled by the renderer.
12pub fn apply_changes(dom: &VirtualDom, msg: &HotReloadMsg) {
13    try_apply_changes(dom, msg).unwrap()
14}
15
16/// Applies template and literal changes to the VirtualDom, but doesn't panic if patching fails.
17///
18/// Assets need to be handled by the renderer.
19pub fn try_apply_changes(dom: &VirtualDom, msg: &HotReloadMsg) -> Result<(), PatchError> {
20    dom.runtime().in_scope(ScopeId::ROOT, || {
21        // 1. Update signals...
22        let ctx = dioxus_signals::get_global_context();
23        for template in &msg.templates {
24            let value = template.template.clone();
25            let key = GlobalKey::File {
26                file: template.key.file.as_str(),
27                line: template.key.line as _,
28                column: template.key.column as _,
29                index: template.key.index as _,
30            };
31            if let Some(mut signal) = ctx.get_signal_with_key(key.clone()) {
32                signal.set(Some(value));
33            }
34        }
35
36        // 2. Attempt to hotpatch
37        if let Some(jump_table) = msg.jump_table.as_ref().cloned() {
38            if msg.for_build_id == Some(dioxus_cli_config::build_id()) {
39                let our_pid = if cfg!(target_family = "wasm") {
40                    None
41                } else {
42                    Some(std::process::id())
43                };
44
45                if msg.for_pid == our_pid {
46                    unsafe { subsecond::apply_patch(jump_table) }?;
47                    dom.runtime().force_all_dirty();
48                    ctx.clear::<Signal<Option<HotReloadedTemplate>>>();
49                }
50            }
51        }
52
53        Ok(())
54    })
55}
56
57/// Connect to the devserver and handle its messages with a callback.
58///
59/// This doesn't use any form of security or protocol, so it's not safe to expose to the internet.
60#[cfg(not(target_family = "wasm"))]
61pub fn connect(callback: impl FnMut(DevserverMsg) + Send + 'static) {
62    let Some(endpoint) = dioxus_cli_config::devserver_ws_endpoint() else {
63        return;
64    };
65
66    connect_at(endpoint, callback);
67}
68
69/// Connect to the devserver and handle hot-patch messages only, implementing the subsecond hotpatch
70/// protocol.
71///
72/// This is intended to be used by non-dioxus projects that want to use hotpatching.
73///
74/// To handle the full devserver protocol, use `connect` instead.
75#[cfg(not(target_family = "wasm"))]
76pub fn connect_subsecond() {
77    connect(|msg| {
78        if let DevserverMsg::HotReload(hot_reload_msg) = msg {
79            if let Some(jumptable) = hot_reload_msg.jump_table {
80                if hot_reload_msg.for_pid == Some(std::process::id()) {
81                    unsafe { subsecond::apply_patch(jumptable).unwrap() };
82                }
83            }
84        }
85    });
86}
87
88#[cfg(not(target_family = "wasm"))]
89pub fn connect_at(endpoint: String, mut callback: impl FnMut(DevserverMsg) + Send + 'static) {
90    std::thread::spawn(move || {
91        let uri = format!(
92            "{endpoint}?aslr_reference={}&build_id={}&pid={}",
93            subsecond::aslr_reference(),
94            dioxus_cli_config::build_id(),
95            std::process::id()
96        );
97
98        let (mut websocket, _req) = match tungstenite::connect(uri) {
99            Ok((websocket, req)) => (websocket, req),
100            Err(_) => return,
101        };
102
103        while let Ok(msg) = websocket.read() {
104            if let tungstenite::Message::Text(text) = msg {
105                if let Ok(msg) = serde_json::from_str(&text) {
106                    callback(msg);
107                }
108            }
109        }
110    });
111}
112
113/// Run this asynchronous future to completion.
114///
115/// Whenever your code changes, the future is dropped and a new one is created using the new function.
116///
117/// This is useful for using subsecond outside of dioxus, like with axum. To pass args to the underlying
118/// function, you can use the `serve_subsecond_with_args` function.
119///
120/// ```rust, ignore
121/// #[tokio::main]
122/// async fn main() {
123///     dioxus_devtools::serve_subsecond(router_main).await;
124/// }
125///
126/// async fn router_main() {
127///     use axum::{Router, routing::get};
128///
129///     let app = Router::new().route("/", get(test_route));
130///
131///     let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
132///     println!("Server running on http://localhost:3000");
133///
134///     axum::serve(listener, app.clone()).await.unwrap()
135/// }
136///
137/// async fn test_route() -> axum::response::Html<&'static str> {
138///     "axum works!!!!!".into()
139/// }
140/// ```
141#[cfg(feature = "serve")]
142#[cfg(not(target_family = "wasm"))]
143pub async fn serve_subsecond<O, F>(mut callback: impl FnMut() -> F)
144where
145    F: std::future::Future<Output = O> + 'static,
146{
147    serve_subsecond_with_args((), move |_args| callback()).await
148}
149
150/// Run this asynchronous future to completion.
151///
152/// Whenever your code changes, the future is dropped and a new one is created using the new function.
153///
154/// ```rust, ignore
155/// #[tokio::main]
156/// async fn main() {
157///     let args = ("abc".to_string(),);
158///     dioxus_devtools::serve_subsecond_with_args(args, router_main).await;
159/// }
160///
161/// async fn router_main(args: (String,)) {
162///     use axum::{Router, routing::get};
163///
164///     let app = Router::new().route("/", get(test_route));
165///
166///     let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
167///     println!("Server running on http://localhost:3000 -> {}", args.0);
168///
169///     axum::serve(listener, app.clone()).await.unwrap()
170/// }
171///
172/// async fn test_route() -> axum::response::Html<&'static str> {
173///     "axum works!!!!!".into()
174/// }
175/// ```
176#[cfg(feature = "serve")]
177pub async fn serve_subsecond_with_args<A: Clone, O, F>(args: A, mut callback: impl FnMut(A) -> F)
178where
179    F: std::future::Future<Output = O> + 'static,
180{
181    let (tx, mut rx) = futures_channel::mpsc::unbounded();
182
183    connect(move |msg| {
184        if let DevserverMsg::HotReload(hot_reload_msg) = msg {
185            if let Some(jumptable) = hot_reload_msg.jump_table {
186                if hot_reload_msg.for_pid == Some(std::process::id()) {
187                    unsafe { subsecond::apply_patch(jumptable).unwrap() };
188                    tx.unbounded_send(()).unwrap();
189                }
190            }
191        }
192    });
193
194    let wrapped = move |args| -> std::pin::Pin<Box<dyn std::future::Future<Output = O>>> {
195        Box::pin(callback(args))
196    };
197
198    let mut hotfn = subsecond::HotFn::current(wrapped);
199    let mut cur_future = hotfn.call((args.clone(),));
200
201    loop {
202        use futures_util::StreamExt;
203        let res = futures_util::future::select(cur_future, rx.next()).await;
204
205        match res {
206            futures_util::future::Either::Left(_completed) => _ = rx.next().await,
207            futures_util::future::Either::Right(_reload) => {}
208        }
209
210        cur_future = hotfn.call((args.clone(),));
211    }
212}