diff --git a/Cargo.lock b/Cargo.lock index 808d16d58..1d5639404 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5686,7 +5686,6 @@ dependencies = [ name = "dioxus-logger" version = "0.7.0-rc.0" dependencies = [ - "console_error_panic_hook", "dioxus", "dioxus-cli-config", "tracing", diff --git a/Cargo.toml b/Cargo.toml index 9d5ef916a..599aec67f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -307,7 +307,6 @@ criterion = { version = "0.6" } cargo_metadata = "0.19.2" parking_lot = "0.12.4" tracing-wasm = "0.2.1" -console_error_panic_hook = "0.1.7" base16 = "0.2.1" digest = "0.10.7" sha2 = "0.10.9" diff --git a/packages/logger/Cargo.toml b/packages/logger/Cargo.toml index ab0e6da21..ac71f4597 100644 --- a/packages/logger/Cargo.toml +++ b/packages/logger/Cargo.toml @@ -22,7 +22,6 @@ default = [] [target.'cfg(target_arch = "wasm32")'.dependencies] tracing-wasm = { workspace = true } -console_error_panic_hook = { workspace = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tracing-subscriber = { workspace = true, features = ["fmt"] } diff --git a/packages/logger/src/lib.rs b/packages/logger/src/lib.rs index bfc7fd6d8..1bdad935c 100644 --- a/packages/logger/src/lib.rs +++ b/packages/logger/src/lib.rs @@ -91,7 +91,6 @@ pub fn init(level: Level) -> Result<(), SetGlobalDefaultError> { let layer = tracing_wasm::WASMLayer::new(layer_config); let reg = Registry::default().with(layer); - console_error_panic_hook::set_once(); set_global_default(reg) } diff --git a/packages/web/src/cfg.rs b/packages/web/src/cfg.rs index 8fd536f4f..305e32177 100644 --- a/packages/web/src/cfg.rs +++ b/packages/web/src/cfg.rs @@ -14,6 +14,7 @@ use wasm_bindgen::JsCast as _; /// ``` pub struct Config { pub(crate) hydrate: bool, + pub(crate) panic_hook: bool, pub(crate) root: ConfigRoot, #[cfg(feature = "document")] pub(crate) history: Option>, @@ -96,6 +97,7 @@ impl Default for Config { root: ConfigRoot::RootName("main".to_string()), #[cfg(feature = "document")] history: None, + panic_hook: true, } } } diff --git a/packages/web/src/devtools.rs b/packages/web/src/devtools.rs index caf20e138..4d9ba62ac 100644 --- a/packages/web/src/devtools.rs +++ b/packages/web/src/devtools.rs @@ -3,12 +3,12 @@ //! 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 std::fmt::Display; -use std::time::Duration; - 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}; @@ -21,15 +21,23 @@ const POLL_INTERVAL_SCALE_FACTOR: i32 = 2; 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() -> UnboundedReceiver { +pub(crate) fn init(config: &crate::Config) -> UnboundedReceiver { // 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 } @@ -223,7 +231,7 @@ pub(crate) fn show_toast( _ = js_sys::eval(&format!( r#" if (typeof {js_fn_name} !== "undefined") {{ - window.{js_fn_name}("{header_text}", "{message}", "{level}", {as_ms}); + window.{js_fn_name}(`{header_text}`, `{message}`, `{level}`, {as_ms}); }} "#, )); @@ -289,3 +297,53 @@ fn playground(tx: UnboundedSender) { 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, + ) +} diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index d63d48a21..5afca0dab 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -51,6 +51,9 @@ pub use hydration::*; /// wasm_bindgen_futures::spawn_local(app_fut); /// ``` pub async fn run(mut virtual_dom: VirtualDom, web_config: Config) -> ! { + #[cfg(all(feature = "devtools", debug_assertions))] + let mut hotreload_rx = devtools::init(&web_config); + #[cfg(feature = "document")] if let Some(history) = web_config.history.clone() { virtual_dom.in_runtime(|| dioxus_core::ScopeId::ROOT.provide_context(history)); @@ -61,9 +64,6 @@ pub async fn run(mut virtual_dom: VirtualDom, web_config: Config) -> ! { let runtime = virtual_dom.runtime(); - #[cfg(all(feature = "devtools", debug_assertions))] - let mut hotreload_rx = devtools::init(); - // If the hydrate feature is enabled, launch the client with hydration enabled let should_hydrate = web_config.hydrate || cfg!(feature = "hydrate");