-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Expand file tree
/
Copy pathdevtools.rs
More file actions
362 lines (315 loc) · 13.1 KB
/
devtools.rs
File metadata and controls
362 lines (315 loc) · 13.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
//! Handler code for hotreloading.
//!
//! This sets up a websocket connection to the devserver and handles messages from it.
//! We also set up a little recursive timer that will attempt to reconnect if the connection is lost.
use dioxus_devtools::{DevserverMsg, HotReloadMsg};
use futures_channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender};
use js_sys::JsString;
use std::fmt::Display;
use std::time::Duration;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use wasm_bindgen::{closure::Closure, JsValue};
use web_sys::{window, CloseEvent, MessageEvent, WebSocket};
const POLL_INTERVAL_MIN: i32 = 250;
const POLL_INTERVAL_MAX: i32 = 4000;
const POLL_INTERVAL_SCALE_FACTOR: i32 = 2;
/// Amount of time that toats should be displayed.
const TOAST_TIMEOUT: Duration = Duration::from_secs(5);
const TOAST_TIMEOUT_LONG: Duration = Duration::from_secs(3600); // Duration::MAX is too long for JS.
pub(crate) fn init(config: &crate::Config) -> UnboundedReceiver<HotReloadMsg> {
// Create the tx/rx pair that we'll use for the top-level future in the dioxus loop
let (tx, rx) = unbounded();
// Wire up the websocket to the devserver
make_ws(tx.clone(), POLL_INTERVAL_MIN, false);
// Set up the playground
playground(tx);
// Set up the panic hook
if config.panic_hook {
std::panic::set_hook(Box::new(|info| {
hook_impl(info);
}));
}
rx
}
fn make_ws(tx: UnboundedSender<HotReloadMsg>, poll_interval: i32, reload: bool) {
// Get the location of the devserver, using the current location plus the /_dioxus path
// The idea here being that the devserver is always located on the /_dioxus behind a proxy
let mut location =
web_sys::Url::new(&web_sys::window().unwrap().location().href().unwrap()).unwrap();
if location.protocol().ends_with("-extension:") {
location = web_sys::Url::new("http://localhost:8080").unwrap();
}
let url = format!(
"{protocol}//{host}/_dioxus?build_id={build_id}",
protocol = match location.protocol() {
prot if prot == "https:" => "wss:",
_ => "ws:",
},
host = location.host(),
build_id = dioxus_cli_config::build_id(),
);
let ws = WebSocket::new(&url).unwrap();
// Set the onmessage handler to bounce messages off to the main dioxus loop
let tx_ = tx.clone();
ws.set_onmessage(Some(
Closure::<dyn FnMut(MessageEvent)>::new(move |e: MessageEvent| {
let Ok(text) = e.data().dyn_into::<JsString>() else {
return;
};
// The devserver messages have some &'static strs in them, so we need to leak the source string
let string: String = text.into();
let string = Box::leak(string.into_boxed_str());
match serde_json::from_str::<DevserverMsg>(string) {
Ok(DevserverMsg::HotReload(hr)) => _ = tx_.unbounded_send(hr),
// todo: we want to throw a screen here that shows the user that the devserver has disconnected
// Would be nice to do that with dioxus itself or some html/css
// But if the dev server shutsdown we don't want to be super aggressive about it... let's
// play with other devservers to see how they handle this
Ok(DevserverMsg::Shutdown) => {
web_sys::console::error_1(&"Connection to the devserver was closed".into())
}
// The devserver is telling us that it started a full rebuild. This does not mean that it is ready.
Ok(DevserverMsg::FullReloadStart) => show_toast(
"Your app is being rebuilt.",
"A non-hot-reloadable change occurred and we must rebuild.",
ToastLevel::Info,
TOAST_TIMEOUT_LONG,
false,
),
// The devserver is telling us that it started a full rebuild. This does not mean that it is ready.
Ok(DevserverMsg::HotPatchStart) => show_toast(
"Hot-patching app...",
"Hot-patching modified Rust code.",
ToastLevel::Info,
TOAST_TIMEOUT_LONG,
false,
),
// The devserver is telling us that the full rebuild failed.
Ok(DevserverMsg::FullReloadFailed) => show_toast(
"Oops! The build failed.",
"We tried to rebuild your app, but something went wrong.",
ToastLevel::Error,
TOAST_TIMEOUT_LONG,
false,
),
// The devserver is telling us to reload the whole page
Ok(DevserverMsg::FullReloadCommand) => {
show_toast(
"Successfully rebuilt.",
"Your app was rebuilt successfully and without error.",
ToastLevel::Success,
TOAST_TIMEOUT,
true,
);
window().unwrap().location().reload().unwrap()
}
Err(e) => web_sys::console::error_1(
&format!("Error parsing devserver message: {}", e).into(),
),
e => {
web_sys::console::error_1(
&format!("Error parsing devserver message: {:?}", e).into(),
);
}
}
})
.into_js_value()
.as_ref()
.unchecked_ref(),
));
// Set the onclose handler to reload the page if the connection is closed
ws.set_onclose(Some(
Closure::<dyn FnMut(CloseEvent)>::new(move |e: CloseEvent| {
// Firefox will send a 1001 code when the connection is closed because the page is reloaded
// Only firefox will trigger the onclose event when the page is reloaded manually: https://stackoverflow.com/questions/10965720/should-websocket-onclose-be-triggered-by-user-navigation-or-refresh
// We should not reload the page in this case
if e.code() == 1001 {
return;
}
// set timeout to reload the page in timeout_ms
let tx = tx.clone();
web_sys::window()
.unwrap()
.set_timeout_with_callback_and_timeout_and_arguments_0(
Closure::<dyn FnMut()>::new(move || {
make_ws(
tx.clone(),
POLL_INTERVAL_MAX.min(poll_interval * POLL_INTERVAL_SCALE_FACTOR),
true,
);
})
.into_js_value()
.as_ref()
.unchecked_ref(),
poll_interval,
)
.unwrap();
})
.into_js_value()
.as_ref()
.unchecked_ref(),
));
// Set the onopen handler to reload the page if the connection is closed
ws.set_onopen(Some(
Closure::<dyn FnMut(MessageEvent)>::new(move |_evt| {
if reload {
window().unwrap().location().reload().unwrap();
}
})
.into_js_value()
.as_ref()
.unchecked_ref(),
));
// monkey patch our console.log / console.error to send the logs to the websocket
// this will let us see the logs in the devserver!
// We only do this if we're not reloading the page, since that will cause duplicate monkey patches
if !reload {
// the method we need to patch:
// https://developer.mozilla.org/en-US/docs/Web/API/Console/log
// log, info, warn, error, debug
let ws: &JsValue = ws.as_ref();
dioxus_interpreter_js::minimal_bindings::monkeyPatchConsole(ws.clone());
}
}
/// Represents what color the toast should have.
pub(crate) enum ToastLevel {
/// Green
Success,
/// Blue
Info,
/// Red
Error,
}
impl Display for ToastLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ToastLevel::Success => write!(f, "success"),
ToastLevel::Info => write!(f, "info"),
ToastLevel::Error => write!(f, "error"),
}
}
}
#[wasm_bindgen(inline_js = r#"
export function js_show_toast(header_text, message, level, as_ms) {
if (typeof showDXToast !== "undefined") {{
window.showDXToast(header_text, message, level, as_ms);
}}
}
export function js_schedule_toast(header_text, message, level, as_ms) {
if (typeof scheduleDXToast !== "undefined") {{
window.scheduleDXToast(header_text, message, level, as_ms);
}}
}
"#)]
extern "C" {
fn js_schedule_toast(header_text: &str, message: &str, level: String, as_ms: u32);
fn js_show_toast(header_text: &str, message: &str, level: String, as_ms: u32);
}
/// Displays a toast to the developer.
pub(crate) fn show_toast(
header_text: &str,
message: &str,
level: ToastLevel,
duration: Duration,
after_reload: bool,
) {
let as_ms: u32 = duration.as_millis().try_into().unwrap();
match after_reload {
true => js_schedule_toast(header_text, message, level.to_string(), as_ms),
false => js_show_toast(header_text, message, level.to_string(), as_ms),
}
}
/// Force a hotreload of the assets on this page by walking them and changing their URLs to include
/// some extra entropy.
///
/// This should... mostly work.
pub(crate) fn invalidate_browser_asset_cache() {
// it might be triggering a reload of assets
// invalidate all the stylesheets on the page
let links = web_sys::window()
.unwrap()
.document()
.unwrap()
.query_selector_all("link[rel=stylesheet]")
.unwrap();
let noise = js_sys::Math::random();
for x in 0..links.length() {
use wasm_bindgen::JsCast;
let link: web_sys::Element = links.get(x).unwrap().unchecked_into();
if let Some(href) = link.get_attribute("href") {
let (url, query) = href.split_once('?').unwrap_or((&href, ""));
let mut query_params: Vec<&str> = query.split('&').collect();
// Remove the old force reload param
query_params.retain(|param| !param.starts_with("dx_force_reload="));
// Add the new force reload param
let force_reload = format!("dx_force_reload={noise}");
query_params.push(&force_reload);
// Rejoin the query
let query = query_params.join("&");
_ = link.set_attribute("href", &format!("{url}?{query}"));
}
}
}
/// Initialize required devtools for dioxus-playground.
///
/// This listens for window message events from other Windows (such as window.top when this is running in an iframe).
fn playground(tx: UnboundedSender<HotReloadMsg>) {
let window = web_sys::window().expect("this code should be running in a web context");
let binding = Closure::<dyn FnMut(MessageEvent)>::new(move |e: MessageEvent| {
let Ok(text) = e.data().dyn_into::<JsString>() else {
return;
};
let string: String = text.into();
let Ok(hr_msg) = serde_json::from_str::<HotReloadMsg>(&string) else {
return;
};
_ = tx.unbounded_send(hr_msg);
});
let callback = binding.as_ref().unchecked_ref();
window
.add_event_listener_with_callback("message", callback)
.expect("event listener should be added successfully");
binding.forget();
}
fn hook_impl(info: &std::panic::PanicHookInfo) {
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn error(msg: String);
type Error;
#[wasm_bindgen(constructor)]
fn new() -> Error;
#[wasm_bindgen(structural, method, getter)]
fn stack(error: &Error) -> String;
}
let mut msg = info.to_string();
// Add the error stack to our message.
//
// This ensures that even if the `console` implementation doesn't
// include stacks for `console.error`, the stack is still available
// for the user. Additionally, Firefox's console tries to clean up
// stack traces, and ruins Rust symbols in the process
// (https://bugzilla.mozilla.org/show_bug.cgi?id=1519569) but since
// it only touches the logged message's associated stack, and not
// the message's contents, by including the stack in the message
// contents we make sure it is available to the user.
msg.push_str("\n\nStack:\n\n");
let e = Error::new();
let stack = e.stack();
msg.push_str(&stack);
// Safari's devtools, on the other hand, _do_ mess with logged
// messages' contents, so we attempt to break their heuristics for
// doing that by appending some whitespace.
// https://github.com/rustwasm/console_error_panic_hook/issues/7
msg.push_str("\n\n");
// Log the panic with `console.error`!
error(msg.clone());
show_toast(
"App panicked! See console for details.",
&msg,
ToastLevel::Error,
TOAST_TIMEOUT_LONG,
false,
)
}