Skip to main content

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            && msg.for_build_id == Some(dioxus_cli_config::build_id())
39        {
40            let our_pid = if cfg!(target_family = "wasm") {
41                None
42            } else {
43                Some(std::process::id())
44            };
45
46            if msg.for_pid == our_pid {
47                tracing::trace!(
48                    "Applying hot-patch jump table with {} entries ({} templates in message)",
49                    jump_table.map.len(),
50                    msg.templates.len()
51                );
52                unsafe { subsecond::apply_patch(jump_table) }?;
53                dom.runtime().force_all_dirty();
54                ctx.clear::<Signal<Option<HotReloadedTemplate>>>();
55            } else {
56                tracing::warn!(
57                    "Ignoring hot-patch intended for pid {:?} (this process is pid {:?})",
58                    msg.for_pid,
59                    our_pid
60                );
61            }
62        }
63
64        Ok(())
65    })
66}
67
68/// Connect to the devserver and handle its messages with a callback.
69///
70/// This doesn't use any form of security or protocol, so it's not safe to expose to the internet.
71#[cfg(not(target_family = "wasm"))]
72pub fn connect(callback: impl FnMut(DevserverMsg) + Send + 'static) {
73    let Some(endpoint) = dioxus_cli_config::devserver_ws_endpoint() else {
74        return;
75    };
76
77    connect_at(endpoint, callback);
78}
79
80/// Connect to the devserver and handle hot-patch messages only, implementing the subsecond hotpatch
81/// protocol.
82///
83/// This is intended to be used by non-dioxus projects that want to use hotpatching.
84///
85/// To handle the full devserver protocol, use `connect` instead.
86#[cfg(not(target_family = "wasm"))]
87pub fn connect_subsecond() {
88    connect(|msg| {
89        if let DevserverMsg::HotReload(hot_reload_msg) = msg
90            && let Some(jumptable) = hot_reload_msg.jump_table
91            && hot_reload_msg.for_pid == Some(std::process::id())
92        {
93            unsafe { subsecond::apply_patch(jumptable).unwrap() };
94        }
95    });
96}
97
98#[cfg(not(target_family = "wasm"))]
99pub fn connect_at(endpoint: String, mut callback: impl FnMut(DevserverMsg) + Send + 'static) {
100    std::thread::spawn(move || {
101        let uri = format!(
102            "{endpoint}?aslr_reference={}&build_id={}&pid={}",
103            subsecond::aslr_reference(),
104            dioxus_cli_config::build_id(),
105            std::process::id()
106        );
107
108        let (mut websocket, _req) = match tungstenite::connect(uri) {
109            Ok((websocket, req)) => (websocket, req),
110            Err(err) => {
111                tracing::warn!("Failed to connect to devserver websocket: {err}");
112                return;
113            }
114        };
115
116        loop {
117            match websocket.read() {
118                Ok(tungstenite::Message::Text(text)) => match serde_json::from_str(&text) {
119                    Ok(msg) => callback(msg),
120                    Err(err) => {
121                        tracing::warn!("Failed to deserialize devserver message: {err}");
122                    }
123                },
124                Ok(_) => {}
125                Err(tungstenite::Error::ConnectionClosed) => {
126                    tracing::debug!("Devserver websocket closed");
127                    return;
128                }
129                Err(err) => {
130                    tracing::warn!(
131                        "Devserver websocket closed - no more hot-reloads or hot-patches will be received: {err}"
132                    );
133                    return;
134                }
135            }
136        }
137    });
138}
139
140/// Run this asynchronous future to completion.
141///
142/// Whenever your code changes, the future is dropped and a new one is created using the new function.
143///
144/// This is useful for using subsecond outside of dioxus, like with axum. To pass args to the underlying
145/// function, you can use the `serve_subsecond_with_args` function.
146///
147/// ```rust, ignore
148/// #[tokio::main]
149/// async fn main() {
150///     dioxus_devtools::serve_subsecond(router_main).await;
151/// }
152///
153/// async fn router_main() {
154///     use axum::{Router, routing::get};
155///
156///     let app = Router::new().route("/", get(test_route));
157///
158///     let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
159///     println!("Server running on http://localhost:3000");
160///
161///     axum::serve(listener, app.clone()).await.unwrap()
162/// }
163///
164/// async fn test_route() -> axum::response::Html<&'static str> {
165///     "axum works!!!!!".into()
166/// }
167/// ```
168#[cfg(feature = "serve")]
169#[cfg(not(target_family = "wasm"))]
170pub async fn serve_subsecond<O, F>(mut callback: impl FnMut() -> F)
171where
172    F: std::future::Future<Output = O> + 'static,
173{
174    serve_subsecond_with_args((), move |_args| callback()).await
175}
176
177/// Run this asynchronous future to completion.
178///
179/// Whenever your code changes, the future is dropped and a new one is created using the new function.
180///
181/// ```rust, ignore
182/// #[tokio::main]
183/// async fn main() {
184///     let args = ("abc".to_string(),);
185///     dioxus_devtools::serve_subsecond_with_args(args, router_main).await;
186/// }
187///
188/// async fn router_main(args: (String,)) {
189///     use axum::{Router, routing::get};
190///
191///     let app = Router::new().route("/", get(test_route));
192///
193///     let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
194///     println!("Server running on http://localhost:3000 -> {}", args.0);
195///
196///     axum::serve(listener, app.clone()).await.unwrap()
197/// }
198///
199/// async fn test_route() -> axum::response::Html<&'static str> {
200///     "axum works!!!!!".into()
201/// }
202/// ```
203#[cfg(feature = "serve")]
204pub async fn serve_subsecond_with_args<A: Clone, O, F>(args: A, mut callback: impl FnMut(A) -> F)
205where
206    F: std::future::Future<Output = O> + 'static,
207{
208    let (tx, mut rx) = futures_channel::mpsc::unbounded();
209
210    connect(move |msg| {
211        if let DevserverMsg::HotReload(hot_reload_msg) = msg
212            && let Some(jumptable) = hot_reload_msg.jump_table
213            && hot_reload_msg.for_pid == Some(std::process::id())
214        {
215            unsafe { subsecond::apply_patch(jumptable).unwrap() };
216            tx.unbounded_send(()).unwrap();
217        }
218    });
219
220    let wrapped = move |args| -> std::pin::Pin<Box<dyn std::future::Future<Output = O>>> {
221        Box::pin(callback(args))
222    };
223
224    let mut hotfn = subsecond::HotFn::current(wrapped);
225    let mut cur_future = hotfn.call((args.clone(),));
226
227    loop {
228        use futures_util::StreamExt;
229        let res = futures_util::future::select(cur_future, rx.next()).await;
230
231        match res {
232            futures_util::future::Either::Left(_completed) => _ = rx.next().await,
233            futures_util::future::Either::Right((None, callback)) => {
234                // Receiving `None` here means that the sender is not connected, which
235                // typically means the dioxus devtools protocol has never connected.
236                // We want to run the future to completion and return instead of
237                // re-running the future constantly in the loop.
238                callback.await;
239                return;
240            }
241            futures_util::future::Either::Right((Some(_), _)) => {}
242        }
243
244        cur_future = hotfn.call((args.clone(),));
245    }
246}