From bd484842bdda5ab57a24c2037f736cae761569a9 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 24 Jul 2024 21:48:30 +0200 Subject: [PATCH] Fix playwright tests (#2695) --- packages/cli/src/builder/mod.rs | 9 +- packages/cli/src/serve/mod.rs | 2 +- packages/cli/src/serve/server.rs | 12 +- packages/core/src/suspense/component.rs | 462 +++++++++--------- packages/desktop/src/document.rs | 4 + packages/fullstack/Cargo.toml | 2 +- packages/fullstack/src/document/mod.rs | 6 +- packages/fullstack/src/document/server.rs | 48 +- packages/fullstack/src/document/web.rs | 4 + packages/fullstack/src/launch.rs | 5 +- packages/fullstack/src/lib.rs | 3 +- packages/fullstack/src/render.rs | 101 ++-- packages/fullstack/src/serve_config.rs | 21 +- packages/html/src/document/mod.rs | 11 + packages/liveview/src/eval.rs | 4 + packages/playwright-tests/fullstack.spec.js | 25 +- .../nested-suspense-no-js.spec.js | 16 +- .../playwright-tests/nested-suspense.spec.js | 4 + .../playwright-tests/playwright.config.js | 4 + .../static-generation/src/main.rs | 2 +- .../suspense-carousel.spec.js | 3 + packages/rsx/src/element.rs | 1 + packages/ssr/src/lib.rs | 9 +- packages/ssr/src/renderer.rs | 21 + packages/static-generation/src/launch.rs | 17 +- packages/static-generation/src/ssg.rs | 5 +- packages/web/src/document.rs | 4 + packages/web/src/launch.rs | 2 +- 28 files changed, 477 insertions(+), 330 deletions(-) diff --git a/packages/cli/src/builder/mod.rs b/packages/cli/src/builder/mod.rs index 21b588dd5..6c7ba3b6d 100644 --- a/packages/cli/src/builder/mod.rs +++ b/packages/cli/src/builder/mod.rs @@ -117,6 +117,7 @@ impl BuildResult { &self, serve: &ServeArguments, fullstack_address: Option, + workspace: &std::path::Path, ) -> std::io::Result> { if self.web { return Ok(None); @@ -124,12 +125,8 @@ impl BuildResult { let arguments = RuntimeCLIArguments::new(serve.address.address(), fullstack_address); let executable = self.executable.canonicalize()?; - // This is the /dist folder generally - let output_folder = executable.parent().unwrap(); - // This is the workspace folder - let workspace_folder = output_folder.parent().unwrap(); Ok(Some( - Command::new(&executable) + Command::new(executable) // When building the fullstack server, we need to forward the serve arguments (like port) to the fullstack server through env vars .env( dioxus_cli_config::__private::SERVE_ENV, @@ -138,7 +135,7 @@ impl BuildResult { .stderr(Stdio::piped()) .stdout(Stdio::piped()) .kill_on_drop(true) - .current_dir(workspace_folder) + .current_dir(workspace) .spawn()?, )) } diff --git a/packages/cli/src/serve/mod.rs b/packages/cli/src/serve/mod.rs index 8d880eedd..dc067bf7b 100644 --- a/packages/cli/src/serve/mod.rs +++ b/packages/cli/src/serve/mod.rs @@ -117,7 +117,7 @@ pub async fn serve_all(serve: Serve, dioxus_crate: DioxusCrate) -> Result<()> { // If we have a build result, open it for build_result in results.iter() { - let child = build_result.open(&serve.server_arguments, server.fullstack_address()); + let child = build_result.open(&serve.server_arguments, server.fullstack_address(), &dioxus_crate.workspace_dir()); match child { Ok(Some(child_proc)) => builder.children.push((build_result.platform,child_proc)), Err(_e) => break, diff --git a/packages/cli/src/serve/server.rs b/packages/cli/src/serve/server.rs index e53e29fb6..5f243ab28 100644 --- a/packages/cli/src/serve/server.rs +++ b/packages/cli/src/serve/server.rs @@ -21,6 +21,7 @@ use axum_server::tls_rustls::RustlsConfig; use dioxus_cli_config::{Platform, WebHttpsConfig}; use dioxus_hot_reload::{DevserverMsg, HotReloadMsg}; use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender}; +use futures_util::stream; use futures_util::{stream::FuturesUnordered, StreamExt}; use hyper::header::ACCEPT; use hyper::HeaderMap; @@ -577,7 +578,9 @@ pub(crate) fn open_browser(base_path: Option, address: SocketAddr, https } fn get_available_port(address: IpAddr) -> Option { - (8000..9000).find(|port| TcpListener::bind((address, *port)).is_ok()) + TcpListener::bind((address, 0)) + .map(|listener| listener.local_addr().unwrap().port()) + .ok() } /// Middleware that intercepts html requests if the status is "Building" and returns a loading page instead @@ -598,7 +601,12 @@ async fn build_status_middleware( let html = include_str!("../../assets/loading.html"); return axum::response::Response::builder() .status(StatusCode::OK) - .body(Body::from(html)) + // Load the html loader then keep loading forever + // We never close the stream so any headless testing framework (like playwright) will wait until the real build is done + .body(Body::from_stream( + stream::once(async move { Ok::<_, std::convert::Infallible>(html) }) + .chain(stream::pending()), + )) .unwrap(); } } diff --git a/packages/core/src/suspense/component.rs b/packages/core/src/suspense/component.rs index 93e0b5335..abdb4e019 100644 --- a/packages/core/src/suspense/component.rs +++ b/packages/core/src/suspense/component.rs @@ -276,7 +276,6 @@ impl SuspenseBoundaryProps { to: Option<&mut M>, ) -> usize { let mut scope_id = ScopeId(dom.mounts[mount.0].mounted_dynamic_nodes[idx]); - // If the ScopeId is a placeholder, we need to load up a new scope for this vcomponent. If it's already mounted, then we can just use that if scope_id.is_placeholder() { { @@ -300,78 +299,80 @@ impl SuspenseBoundaryProps { // Store the scope id for the next render dom.mounts[mount.0].mounted_dynamic_nodes[idx] = scope_id.0; } - - let scope_state = &mut dom.scopes[scope_id.0]; - let props = Self::downcast_from_props(&mut *scope_state.props).unwrap(); - let suspense_context = - SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id).unwrap(); - - let children = RenderReturn { - node: props - .children - .as_ref() - .map(|node| node.clone_mounted()) - .map_err(Clone::clone), - }; - - // First always render the children in the background. Rendering the children may cause this boundary to suspend - suspense_context.under_suspense_boundary(&dom.runtime(), || { - children.create(dom, parent, None::<&mut M>); - }); - - // Store the (now mounted) children back into the scope state - let scope_state = &mut dom.scopes[scope_id.0]; - let props = Self::downcast_from_props(&mut *scope_state.props).unwrap(); - props.children = children.clone().node; - - let scope_state = &mut dom.scopes[scope_id.0]; - let suspense_context = scope_state - .state() - .suspense_location() - .suspense_context() - .unwrap() - .clone(); - // If there are suspended futures, render the fallback - let nodes_created = if !suspense_context.suspended_futures().is_empty() { - let (node, nodes_created) = - suspense_context.in_suspense_placeholder(&dom.runtime(), || { - let scope_state = &mut dom.scopes[scope_id.0]; - let props = Self::downcast_from_props(&mut *scope_state.props).unwrap(); - let suspense_context = SuspenseContext::downcast_suspense_boundary_from_scope( - &dom.runtime, - scope_id, - ) - .unwrap(); - suspense_context.set_suspended_nodes(children.into()); - let suspense_placeholder = props.fallback.call(suspense_context); - let node = RenderReturn { - node: suspense_placeholder, - }; - let nodes_created = node.create(dom, parent, to); - (node, nodes_created) - }); - + dom.runtime.clone().with_scope_on_stack(scope_id, || { let scope_state = &mut dom.scopes[scope_id.0]; - scope_state.last_rendered_node = Some(node); - - nodes_created - } else { - // Otherwise just render the children in the real dom - debug_assert!(children.mount.get().mounted()); - let nodes_created = suspense_context - .under_suspense_boundary(&dom.runtime(), || children.create(dom, parent, to)); - let scope_state = &mut dom.scopes[scope_id.0]; - scope_state.last_rendered_node = Some(children); + let props = Self::downcast_from_props(&mut *scope_state.props).unwrap(); let suspense_context = SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id) .unwrap(); - suspense_context.take_suspended_nodes(); - mark_suspense_resolved(dom, scope_id); + let children = RenderReturn { + node: props + .children + .as_ref() + .map(|node| node.clone_mounted()) + .map_err(Clone::clone), + }; + + // First always render the children in the background. Rendering the children may cause this boundary to suspend + suspense_context.under_suspense_boundary(&dom.runtime(), || { + children.create(dom, parent, None::<&mut M>); + }); + + // Store the (now mounted) children back into the scope state + let scope_state = &mut dom.scopes[scope_id.0]; + let props = Self::downcast_from_props(&mut *scope_state.props).unwrap(); + props.children = children.clone().node; + + let scope_state = &mut dom.scopes[scope_id.0]; + let suspense_context = scope_state + .state() + .suspense_location() + .suspense_context() + .unwrap() + .clone(); + // If there are suspended futures, render the fallback + let nodes_created = if !suspense_context.suspended_futures().is_empty() { + let (node, nodes_created) = + suspense_context.in_suspense_placeholder(&dom.runtime(), || { + let scope_state = &mut dom.scopes[scope_id.0]; + let props = Self::downcast_from_props(&mut *scope_state.props).unwrap(); + let suspense_context = + SuspenseContext::downcast_suspense_boundary_from_scope( + &dom.runtime, + scope_id, + ) + .unwrap(); + suspense_context.set_suspended_nodes(children.into()); + let suspense_placeholder = props.fallback.call(suspense_context); + let node = RenderReturn { + node: suspense_placeholder, + }; + let nodes_created = node.create(dom, parent, to); + (node, nodes_created) + }); + + let scope_state = &mut dom.scopes[scope_id.0]; + scope_state.last_rendered_node = Some(node); + + nodes_created + } else { + // Otherwise just render the children in the real dom + debug_assert!(children.mount.get().mounted()); + let nodes_created = suspense_context + .under_suspense_boundary(&dom.runtime(), || children.create(dom, parent, to)); + let scope_state = &mut dom.scopes[scope_id.0]; + scope_state.last_rendered_node = Some(children); + let suspense_context = + SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id) + .unwrap(); + suspense_context.take_suspended_nodes(); + mark_suspense_resolved(dom, scope_id); + + nodes_created + }; nodes_created - }; - - nodes_created + }) } #[doc(hidden)] @@ -385,65 +386,69 @@ impl SuspenseBoundaryProps { only_write_templates: impl FnOnce(&mut M), replace_with: usize, ) { - let _runtime = RuntimeGuard::new(dom.runtime()); - let Some(scope_state) = dom.scopes.get_mut(scope_id.0) else { - return; - }; + dom.runtime.clone().with_scope_on_stack(scope_id, || { + let _runtime = RuntimeGuard::new(dom.runtime()); + let Some(scope_state) = dom.scopes.get_mut(scope_id.0) else { + return; + }; - // Reset the suspense context - let suspense_context = scope_state - .state() - .suspense_location() - .suspense_context() - .unwrap() - .clone(); - suspense_context.inner.suspended_tasks.borrow_mut().clear(); + // Reset the suspense context + let suspense_context = scope_state + .state() + .suspense_location() + .suspense_context() + .unwrap() + .clone(); + suspense_context.inner.suspended_tasks.borrow_mut().clear(); - // Get the parent of the suspense boundary to later create children with the right parent - let currently_rendered = scope_state.last_rendered_node.as_ref().unwrap().clone(); - let mount = currently_rendered.mount.get(); - let parent = dom - .mounts - .get(mount.0) - .expect("suspense placeholder is not mounted") - .parent; + // Get the parent of the suspense boundary to later create children with the right parent + let currently_rendered = scope_state.last_rendered_node.as_ref().unwrap().clone(); + let mount = currently_rendered.mount.get(); + let parent = dom + .mounts + .get(mount.0) + .expect("suspense placeholder is not mounted") + .parent; - let props = Self::downcast_from_props(&mut *scope_state.props).unwrap(); + let props = Self::downcast_from_props(&mut *scope_state.props).unwrap(); - // Unmount any children to reset any scopes under this suspense boundary - let children = props - .children - .as_ref() - .map(|node| node.clone_mounted()) - .map_err(Clone::clone); - let suspense_context = - SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id).unwrap(); - let suspended = suspense_context.suspended_nodes(); - if let Some(node) = suspended { - node.remove_node(&mut *dom, None::<&mut M>, None); - } - // Replace the rendered nodes with resolved nodes - currently_rendered.remove_node(&mut *dom, Some(to), Some(replace_with)); + // Unmount any children to reset any scopes under this suspense boundary + let children = props + .children + .as_ref() + .map(|node| node.clone_mounted()) + .map_err(Clone::clone); + let suspense_context = + SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id) + .unwrap(); + let suspended = suspense_context.suspended_nodes(); + if let Some(node) = suspended { + node.remove_node(&mut *dom, None::<&mut M>, None); + } + // Replace the rendered nodes with resolved nodes + currently_rendered.remove_node(&mut *dom, Some(to), Some(replace_with)); - // Switch to only writing templates - only_write_templates(to); + // Switch to only writing templates + only_write_templates(to); - let children = RenderReturn { node: children }; - children.mount.take(); + let children = RenderReturn { node: children }; + children.mount.take(); - // First always render the children in the background. Rendering the children may cause this boundary to suspend - suspense_context.under_suspense_boundary(&dom.runtime(), || { - children.create(dom, parent, Some(to)); - }); + // First always render the children in the background. Rendering the children may cause this boundary to suspend + suspense_context.under_suspense_boundary(&dom.runtime(), || { + children.create(dom, parent, Some(to)); + }); - // Store the (now mounted) children back into the scope state - let scope_state = &mut dom.scopes[scope_id.0]; - let props = Self::downcast_from_props(&mut *scope_state.props).unwrap(); - props.children = children.clone().node; - scope_state.last_rendered_node = Some(children); - let suspense_context = - SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id).unwrap(); - suspense_context.take_suspended_nodes(); + // Store the (now mounted) children back into the scope state + let scope_state = &mut dom.scopes[scope_id.0]; + let props = Self::downcast_from_props(&mut *scope_state.props).unwrap(); + props.children = children.clone().node; + scope_state.last_rendered_node = Some(children); + let suspense_context = + SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id) + .unwrap(); + suspense_context.take_suspended_nodes(); + }) } pub(crate) fn diff( @@ -451,127 +456,140 @@ impl SuspenseBoundaryProps { dom: &mut VirtualDom, to: Option<&mut M>, ) { - let scope = &mut dom.scopes[scope_id.0]; - let myself = Self::downcast_from_props(&mut *scope.props) - .unwrap() - .clone(); + dom.runtime.clone().with_scope_on_stack(scope_id, || { + let scope = &mut dom.scopes[scope_id.0]; + let myself = Self::downcast_from_props(&mut *scope.props) + .unwrap() + .clone(); - let last_rendered_node = scope.last_rendered_node.as_ref().unwrap().clone_mounted(); + let last_rendered_node = scope.last_rendered_node.as_ref().unwrap().clone_mounted(); - let Self { - fallback, children, .. - } = myself; + let Self { + fallback, children, .. + } = myself; - let suspense_context = scope.state().suspense_boundary().unwrap().clone(); - let suspended_nodes = suspense_context.suspended_nodes(); - let suspended = !suspense_context.suspended_futures().is_empty(); - match (suspended_nodes, suspended) { - // We already have suspended nodes that still need to be suspended - // Just diff the normal and suspended nodes - (Some(suspended_nodes), true) => { - let new_suspended_nodes: VNode = RenderReturn { node: children }.into(); + let suspense_context = scope.state().suspense_boundary().unwrap().clone(); + let suspended_nodes = suspense_context.suspended_nodes(); + let suspended = !suspense_context.suspended_futures().is_empty(); + match (suspended_nodes, suspended) { + // We already have suspended nodes that still need to be suspended + // Just diff the normal and suspended nodes + (Some(suspended_nodes), true) => { + let new_suspended_nodes: VNode = RenderReturn { node: children }.into(); - // Diff the placeholder nodes in the dom - let new_placeholder = - suspense_context.in_suspense_placeholder(&dom.runtime(), || { - let old_placeholder = last_rendered_node; - let new_placeholder = RenderReturn { - node: fallback.call(suspense_context.clone()), - }; + // Diff the placeholder nodes in the dom + let new_placeholder = + suspense_context.in_suspense_placeholder(&dom.runtime(), || { + let old_placeholder = last_rendered_node; + let new_placeholder = RenderReturn { + node: fallback.call(suspense_context.clone()), + }; - old_placeholder.diff_node(&new_placeholder, dom, to); - new_placeholder + old_placeholder.diff_node(&new_placeholder, dom, to); + new_placeholder + }); + + // Set the last rendered node to the placeholder + dom.scopes[scope_id.0].last_rendered_node = Some(new_placeholder); + + // Diff the suspended nodes in the background + suspense_context.under_suspense_boundary(&dom.runtime(), || { + suspended_nodes.diff_node(&new_suspended_nodes, dom, None::<&mut M>); }); - // Set the last rendered node to the placeholder - dom.scopes[scope_id.0].last_rendered_node = Some(new_placeholder); + let suspense_context = SuspenseContext::downcast_suspense_boundary_from_scope( + &dom.runtime, + scope_id, + ) + .unwrap(); + suspense_context.set_suspended_nodes(new_suspended_nodes); + } + // We have no suspended nodes, and we are not suspended. Just diff the children like normal + (None, false) => { + let old_children = last_rendered_node; + let new_children = RenderReturn { node: children }; - // Diff the suspended nodes in the background - suspense_context.under_suspense_boundary(&dom.runtime(), || { - suspended_nodes.diff_node(&new_suspended_nodes, dom, None::<&mut M>); - }); + suspense_context.under_suspense_boundary(&dom.runtime(), || { + old_children.diff_node(&new_children, dom, to); + }); - let suspense_context = - SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id) - .unwrap(); - suspense_context.set_suspended_nodes(new_suspended_nodes); - } - // We have no suspended nodes, and we are not suspended. Just diff the children like normal - (None, false) => { - let old_children = last_rendered_node; - let new_children = RenderReturn { node: children }; + // Set the last rendered node to the new children + dom.scopes[scope_id.0].last_rendered_node = Some(new_children); + } + // We have no suspended nodes, but we just became suspended. Move the children to the background + (None, true) => { + let old_children = last_rendered_node; + let new_children: VNode = RenderReturn { node: children }.into(); - suspense_context.under_suspense_boundary(&dom.runtime(), || { - old_children.diff_node(&new_children, dom, to); - }); + let new_placeholder = RenderReturn { + node: fallback.call(suspense_context.clone()), + }; - // Set the last rendered node to the new children - dom.scopes[scope_id.0].last_rendered_node = Some(new_children); - } - // We have no suspended nodes, but we just became suspended. Move the children to the background - (None, true) => { - let old_children = last_rendered_node; - let new_children: VNode = RenderReturn { node: children }.into(); - - let new_placeholder = RenderReturn { - node: fallback.call(suspense_context.clone()), - }; - - // Move the children to the background - let mount = old_children.mount.get(); - let mount = dom.mounts.get(mount.0).expect("mount should exist"); - let parent = mount.parent; - - suspense_context.in_suspense_placeholder(&dom.runtime(), || { - old_children.move_node_to_background( - std::slice::from_ref(&*new_placeholder), - parent, - dom, - to, - ); - }); - - // Then diff the new children in the background - suspense_context.under_suspense_boundary(&dom.runtime(), || { - old_children.diff_node(&new_children, dom, None::<&mut M>); - }); - - // Set the last rendered node to the new suspense placeholder - dom.scopes[scope_id.0].last_rendered_node = Some(new_placeholder); - - let suspense_context = - SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id) - .unwrap(); - suspense_context.set_suspended_nodes(new_children); - - un_resolve_suspense(dom, scope_id); - } // We have suspended nodes, but we just got out of suspense. Move the suspended nodes to the foreground - (Some(old_suspended_nodes), false) => { - let old_placeholder = last_rendered_node; - let new_children = RenderReturn { node: children }; - - // First diff the two children nodes in the background - suspense_context.under_suspense_boundary(&dom.runtime(), || { - old_suspended_nodes.diff_node(&new_children, dom, None::<&mut M>); - - // Then replace the placeholder with the new children - let mount = old_placeholder.mount.get(); + // Move the children to the background + let mount = old_children.mount.get(); let mount = dom.mounts.get(mount.0).expect("mount should exist"); let parent = mount.parent; - old_placeholder.replace(std::slice::from_ref(&*new_children), parent, dom, to); - }); - // Set the last rendered node to the new children - dom.scopes[scope_id.0].last_rendered_node = Some(new_children); + suspense_context.in_suspense_placeholder(&dom.runtime(), || { + old_children.move_node_to_background( + std::slice::from_ref(&*new_placeholder), + parent, + dom, + to, + ); + }); - let suspense_context = - SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id) - .unwrap(); - suspense_context.take_suspended_nodes(); + // Then diff the new children in the background + suspense_context.under_suspense_boundary(&dom.runtime(), || { + old_children.diff_node(&new_children, dom, None::<&mut M>); + }); - mark_suspense_resolved(dom, scope_id); + // Set the last rendered node to the new suspense placeholder + dom.scopes[scope_id.0].last_rendered_node = Some(new_placeholder); + + let suspense_context = SuspenseContext::downcast_suspense_boundary_from_scope( + &dom.runtime, + scope_id, + ) + .unwrap(); + suspense_context.set_suspended_nodes(new_children); + + un_resolve_suspense(dom, scope_id); + } // We have suspended nodes, but we just got out of suspense. Move the suspended nodes to the foreground + (Some(old_suspended_nodes), false) => { + let old_placeholder = last_rendered_node; + let new_children = RenderReturn { node: children }; + + // First diff the two children nodes in the background + suspense_context.under_suspense_boundary(&dom.runtime(), || { + old_suspended_nodes.diff_node(&new_children, dom, None::<&mut M>); + + // Then replace the placeholder with the new children + let mount = old_placeholder.mount.get(); + let mount = dom.mounts.get(mount.0).expect("mount should exist"); + let parent = mount.parent; + old_placeholder.replace( + std::slice::from_ref(&*new_children), + parent, + dom, + to, + ); + }); + + // Set the last rendered node to the new children + dom.scopes[scope_id.0].last_rendered_node = Some(new_children); + + let suspense_context = SuspenseContext::downcast_suspense_boundary_from_scope( + &dom.runtime, + scope_id, + ) + .unwrap(); + suspense_context.take_suspended_nodes(); + + mark_suspense_resolved(dom, scope_id); + } } - } + }) } } diff --git a/packages/desktop/src/document.rs b/packages/desktop/src/document.rs index 541b87314..707b9d832 100644 --- a/packages/desktop/src/document.rs +++ b/packages/desktop/src/document.rs @@ -22,6 +22,10 @@ impl Document for DesktopDocument { fn set_title(&self, title: String) { self.desktop_ctx.window.set_title(&title); } + + fn as_any(&self) -> &dyn std::any::Any { + self + } } /// Represents a desktop-target's JavaScript evaluator. diff --git a/packages/fullstack/Cargo.toml b/packages/fullstack/Cargo.toml index 0865d279b..5cfa89890 100644 --- a/packages/fullstack/Cargo.toml +++ b/packages/fullstack/Cargo.toml @@ -104,7 +104,7 @@ server = [ "dep:parking_lot", "dioxus-interpreter-js", "dep:clap", - "dioxus-cli-config/read-from-args" + "dioxus-cli-config/read-from-args", ] [package.metadata.docs.rs] diff --git a/packages/fullstack/src/document/mod.rs b/packages/fullstack/src/document/mod.rs index 37efdc544..0c380994f 100644 --- a/packages/fullstack/src/document/mod.rs +++ b/packages/fullstack/src/document/mod.rs @@ -1,4 +1,8 @@ +//! This module contains the document providers for the fullstack platform. + #[cfg(feature = "server")] pub(crate) mod server; -#[cfg(feature = "web")] +#[cfg(feature = "server")] +pub use server::ServerDocument; +#[cfg(all(feature = "web", feature = "document"))] pub(crate) mod web; diff --git a/packages/fullstack/src/document/server.rs b/packages/fullstack/src/document/server.rs index efcea5884..265cb2a51 100644 --- a/packages/fullstack/src/document/server.rs +++ b/packages/fullstack/src/document/server.rs @@ -7,6 +7,10 @@ use std::cell::RefCell; use dioxus_lib::{html::document::*, prelude::*}; use dioxus_ssr::Renderer; use generational_box::GenerationalBox; +use once_cell::sync::Lazy; +use parking_lot::RwLock; + +static RENDERER: Lazy> = Lazy::new(|| RwLock::new(Renderer::new())); #[derive(Default)] struct ServerDocumentInner { @@ -19,35 +23,27 @@ struct ServerDocumentInner { /// A Document provider that collects all contents injected into the head for SSR rendering. #[derive(Default)] -pub(crate) struct ServerDocument(RefCell); +pub struct ServerDocument(RefCell); impl ServerDocument { - pub(crate) fn render( - &self, - to: &mut impl std::fmt::Write, - renderer: &mut Renderer, - ) -> std::fmt::Result { - fn lazy_app(props: Element) -> Element { - props - } + pub(crate) fn title(&self) -> Option { + let myself = self.0.borrow(); + myself.title.as_ref().map(|title| { + RENDERER + .write() + .render_element(rsx! { title { "{title}" } }) + }) + } + + pub(crate) fn render(&self, to: &mut impl std::fmt::Write) -> std::fmt::Result { let myself = self.0.borrow(); let element = rsx! { - if let Some(title) = myself.title.as_ref() { - title { title: "{title}" } - } {myself.meta.iter().map(|m| rsx! { {m} })} {myself.link.iter().map(|l| rsx! { {l} })} {myself.script.iter().map(|s| rsx! { {s} })} }; - let mut dom = VirtualDom::new_with_props(lazy_app, element); - dom.rebuild_in_place(); - - // We don't hydrate the head, so we can set the pre_render flag to false to save a few bytes - let was_pre_rendering = renderer.pre_render; - renderer.pre_render = false; - renderer.render_to(to, &dom)?; - renderer.pre_render = was_pre_rendering; + RENDERER.write().render_element_to(to, element)?; Ok(()) } @@ -65,8 +61,12 @@ impl ServerDocument { /// Write the head element into the serialized context for hydration /// We write true if the head element was written to the DOM during server side rendering pub(crate) fn serialize_for_hydration(&self) { - let serialize = crate::html_storage::serialize_context(); - serialize.push(&!self.0.borrow().streaming); + // We only serialize the head elements if the web document feature is enabled + #[cfg(feature = "document")] + { + let serialize = crate::html_storage::serialize_context(); + serialize.push(&!self.0.borrow().streaming); + } } } @@ -137,4 +137,8 @@ impl Document for ServerDocument { } }) } + + fn as_any(&self) -> &dyn std::any::Any { + self + } } diff --git a/packages/fullstack/src/document/web.rs b/packages/fullstack/src/document/web.rs index 768816138..3169fa05a 100644 --- a/packages/fullstack/src/document/web.rs +++ b/packages/fullstack/src/document/web.rs @@ -55,4 +55,8 @@ impl Document for FullstackWebDocument { } WebDocument.create_link(props); } + + fn as_any(&self) -> &dyn std::any::Any { + self + } } diff --git a/packages/fullstack/src/launch.rs b/packages/fullstack/src/launch.rs index 32f978094..889c010ca 100644 --- a/packages/fullstack/src/launch.rs +++ b/packages/fullstack/src/launch.rs @@ -62,8 +62,9 @@ pub fn launch( #[cfg(feature = "document")] let factory = move || { let mut vdom = factory(); - vdom.provide_root_context(std::rc::Rc::new(crate::document::web::FullstackWebDocument) - as std::rc::Rc); + let document = std::rc::Rc::new(crate::document::web::FullstackWebDocument) + as std::rc::Rc; + vdom.provide_root_context(document); vdom }; diff --git a/packages/fullstack/src/lib.rs b/packages/fullstack/src/lib.rs index fec63e5b1..c7794ac4a 100644 --- a/packages/fullstack/src/lib.rs +++ b/packages/fullstack/src/lib.rs @@ -18,8 +18,7 @@ pub mod launch; pub use config::*; -#[cfg(feature = "document")] -mod document; +pub mod document; #[cfg(feature = "server")] mod render; #[cfg(feature = "server")] diff --git a/packages/fullstack/src/render.rs b/packages/fullstack/src/render.rs index 3f7935906..43dc5c489 100644 --- a/packages/fullstack/src/render.rs +++ b/packages/fullstack/src/render.rs @@ -7,9 +7,9 @@ use dioxus_ssr::{ }; use futures_channel::mpsc::Sender; use futures_util::{Stream, StreamExt}; +use std::sync::Arc; use std::sync::RwLock; use std::{collections::HashMap, future::Future}; -use std::{fmt::Write, sync::Arc}; use tokio::task::JoinHandle; use crate::prelude::*; @@ -160,9 +160,7 @@ impl SsrRendererPool { let join_handle = spawn_platform(move || async move { let mut virtual_dom = virtual_dom_factory(); - #[cfg(feature = "document")] let document = std::rc::Rc::new(crate::document::server::ServerDocument::default()); - #[cfg(feature = "document")] virtual_dom.provide_root_context(document.clone() as std::rc::Rc); // poll the future, which may call server_context() @@ -171,34 +169,11 @@ impl SsrRendererPool { let mut pre_body = String::new(); - if let Err(err) = wrapper.render_head(&mut pre_body) { + if let Err(err) = wrapper.render_head(&mut pre_body, &virtual_dom) { _ = into.start_send(Err(err)); return; } - #[cfg(feature = "document")] - { - // Collect any head content from the document provider and inject that into the head - if let Err(err) = document.render(&mut pre_body, &mut renderer) { - _ = into.start_send(Err(err.into())); - return; - } - - // Enable a warning when inserting contents into the head during streaming - document.start_streaming(); - } - - if let Err(err) = wrapper.render_before_body(&mut pre_body) { - _ = into.start_send(Err(err)); - return; - } - if let Err(err) = write!(&mut pre_body, "") { - _ = into.start_send(Err( - dioxus_ssr::incremental::IncrementalRendererError::RenderError(err), - )); - return; - } - let stream = Arc::new(StreamingRenderer::new(pre_body, into)); let scope_to_mount_mapping = Arc::new(RwLock::new(HashMap::new())); @@ -237,15 +212,8 @@ impl SsrRendererPool { // Render the initial frame with loading placeholders let mut initial_frame = renderer.render(&virtual_dom); - // Collect the initial server data from the root node. For most apps, no use_server_futures will be resolved initially, so this will be full on `None`s. - // Sending down those Nones are still important to tell the client not to run the use_server_futures that are already running on the backend - let resolved_data = serialize_server_data(&virtual_dom, ScopeId::ROOT); - initial_frame.push_str(&format!( - r#""#, - )); - // Along with the initial frame, we render the html after the main element, but before the body tag closes. This should include the script that starts loading the wasm bundle. - if let Err(err) = wrapper.render_after_main(&mut initial_frame) { + if let Err(err) = wrapper.render_after_main(&mut initial_frame, &virtual_dom) { throw_error!(err); } stream.render(initial_frame); @@ -311,7 +279,7 @@ impl SsrRendererPool { // If incremental rendering is enabled, add the new render to the cache without the streaming bits if let Some(incremental) = &self.incremental_cache { let mut cached_render = String::new(); - if let Err(err) = wrapper.render_before_body(&mut cached_render) { + if let Err(err) = wrapper.render_head(&mut cached_render, &virtual_dom) { throw_error!(err); } cached_render.push_str(&post_streaming); @@ -401,16 +369,48 @@ impl FullstackHTMLTemplate { pub fn render_head( &self, to: &mut R, + virtual_dom: &VirtualDom, ) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> { let ServeConfig { index, .. } = &self.cfg; - to.write_str(&index.head)?; + let title = { + let document: Option> = + virtual_dom.in_runtime(|| ScopeId::ROOT.consume_context()); + let document: Option<&crate::document::server::ServerDocument> = document + .as_ref() + .and_then(|document| document.as_any().downcast_ref()); + // Collect any head content from the document provider and inject that into the head + document.and_then(|document| document.title()) + }; + + to.write_str(&index.head_before_title)?; + if let Some(title) = title { + to.write_str(&title)?; + } else { + to.write_str(&index.title)?; + } + to.write_str(&index.head_after_title)?; + + let document: Option> = + virtual_dom.in_runtime(|| ScopeId::ROOT.consume_context()); + let document: Option<&crate::document::server::ServerDocument> = document + .as_ref() + .and_then(|document| document.as_any().downcast_ref()); + if let Some(document) = document { + // Collect any head content from the document provider and inject that into the head + document.render(to)?; + + // Enable a warning when inserting contents into the head during streaming + document.start_streaming(); + } + + self.render_before_body(to)?; Ok(()) } /// Render any content before the body of the page. - pub fn render_before_body( + fn render_before_body( &self, to: &mut R, ) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> { @@ -418,6 +418,8 @@ impl FullstackHTMLTemplate { to.write_str(&index.close_head)?; + write!(to, "")?; + Ok(()) } @@ -425,9 +427,17 @@ impl FullstackHTMLTemplate { pub fn render_after_main( &self, to: &mut R, + virtual_dom: &VirtualDom, ) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> { let ServeConfig { index, .. } = &self.cfg; + // Collect the initial server data from the root node. For most apps, no use_server_futures will be resolved initially, so this will be full on `None`s. + // Sending down those Nones are still important to tell the client not to run the use_server_futures that are already running on the backend + let resolved_data = serialize_server_data(virtual_dom, ScopeId::ROOT); + write!( + to, + r#""#, + )?; to.write_str(&index.post_main)?; Ok(()) @@ -444,6 +454,21 @@ impl FullstackHTMLTemplate { Ok(()) } + + /// Wrap a body in the template + pub fn wrap_body( + &self, + to: &mut R, + virtual_dom: &VirtualDom, + body: impl std::fmt::Display, + ) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> { + self.render_head(to, virtual_dom)?; + write!(to, "{body}")?; + self.render_after_main(to, virtual_dom)?; + self.render_after_body(to)?; + + Ok(()) + } } fn pre_renderer() -> Renderer { diff --git a/packages/fullstack/src/serve_config.rs b/packages/fullstack/src/serve_config.rs index 0d80aa325..aadc8638d 100644 --- a/packages/fullstack/src/serve_config.rs +++ b/packages/fullstack/src/serve_config.rs @@ -118,8 +118,23 @@ fn load_index_html(contents: String, root_id: &'static str) -> IndexHtml { panic!("Failed to find closing tag after id=\"{root_id}\" in index.html.") }); + // Strip out the head if it exists + let mut head_before_title = String::new(); + let mut head_after_title = head; + let mut title = String::new(); + if let Some((new_head_before_title, new_title)) = head_after_title.split_once("") { + let (new_title, new_head_after_title) = new_title + .split_once("") + .expect("Failed to find closing tag after in index.html."); + title = format!("<title>{new_title}"); + head_before_title = new_head_before_title.to_string(); + head_after_title = new_head_after_title.to_string(); + } + IndexHtml { - head, + head_before_title, + head_after_title, + title, close_head, post_main: post_main.to_string(), after_closing_body_tag: "".to_string() + after_closing_body_tag, @@ -128,7 +143,9 @@ fn load_index_html(contents: String, root_id: &'static str) -> IndexHtml { #[derive(Clone)] pub(crate) struct IndexHtml { - pub(crate) head: String, + pub(crate) head_before_title: String, + pub(crate) head_after_title: String, + pub(crate) title: String, pub(crate) close_head: String, pub(crate) post_main: String, pub(crate) after_closing_body_tag: String, diff --git a/packages/html/src/document/mod.rs b/packages/html/src/document/mod.rs index ab3a662dd..27c05c550 100644 --- a/packages/html/src/document/mod.rs +++ b/packages/html/src/document/mod.rs @@ -51,29 +51,36 @@ pub trait Document { self.new_evaluator(format!("document.title = {title:?};")); } + /// Create a new meta tag fn create_meta(&self, props: MetaProps) { let attributes = props.attributes(); let js = create_element_in_head("meta", &attributes, None); self.new_evaluator(js); } + /// Create a new script tag fn create_script(&self, props: ScriptProps) { let attributes = props.attributes(); let js = create_element_in_head("script", &attributes, props.script_contents()); self.new_evaluator(js); } + /// Create a new style tag fn create_style(&self, props: StyleProps) { let attributes = props.attributes(); let js = create_element_in_head("style", &attributes, props.style_contents()); self.new_evaluator(js); } + /// Create a new link tag fn create_link(&self, props: head::LinkProps) { let attributes = props.attributes(); let js = create_element_in_head("link", &attributes, None); self.new_evaluator(js); } + + /// Get a reference to the document as `dyn Any` + fn as_any(&self) -> &dyn std::any::Any; } /// The default No-Op document @@ -84,6 +91,10 @@ impl Document for NoOpDocument { tracing::error!("Eval is not supported on this platform. If you are using dioxus fullstack, you can wrap your code with `client! {{}}` to only include the code that runs eval in the client bundle."); UnsyncStorage::owner().insert(Box::new(NoOpEvaluator)) } + + fn as_any(&self) -> &dyn std::any::Any { + self + } } struct NoOpEvaluator; diff --git a/packages/liveview/src/eval.rs b/packages/liveview/src/eval.rs index 2f1c1fb1e..6821d6502 100644 --- a/packages/liveview/src/eval.rs +++ b/packages/liveview/src/eval.rs @@ -21,6 +21,10 @@ impl Document for LiveviewDocument { fn new_evaluator(&self, js: String) -> GenerationalBox> { LiveviewEvaluator::create(self.query.clone(), js) } + + fn as_any(&self) -> &dyn std::any::Any { + self + } } /// Represents a liveview-target's JavaScript evaluator. diff --git a/packages/playwright-tests/fullstack.spec.js b/packages/playwright-tests/fullstack.spec.js index 98f7623a2..748436044 100644 --- a/packages/playwright-tests/fullstack.spec.js +++ b/packages/playwright-tests/fullstack.spec.js @@ -1,12 +1,14 @@ // @ts-check const { test, expect } = require("@playwright/test"); -test("button click", async ({ page }) => { +test("hydration", async ({ page }) => { await page.goto("http://localhost:3333"); - await page.waitForTimeout(1000); + + // Expect the page to contain the pending text. + const main = page.locator("#main"); + await expect(main).toContainText("Server said: ..."); // Expect the page to contain the counter text. - const main = page.locator("#main"); await expect(main).toContainText("hello axum! 12345"); // Expect the title to contain the counter text. await expect(page).toHaveTitle("hello axum! 12345"); @@ -15,23 +17,14 @@ test("button click", async ({ page }) => { let button = page.locator("button.increment-button"); await button.click(); + // Click the server button. + let serverButton = page.locator("button.server-button"); + await serverButton.click(); + // Expect the page to contain the updated counter text. await expect(main).toContainText("hello axum! 12346"); // Expect the title to contain the updated counter text. await expect(page).toHaveTitle("hello axum! 12346"); -}); - -test("fullstack communication", async ({ page }) => { - await page.goto("http://localhost:3333"); - await page.waitForTimeout(1000); - - // Expect the page to contain the counter text. - const main = page.locator("#main"); - await expect(main).toContainText("Server said: ..."); - - // Click the increment button. - let button = page.locator("button.server-button"); - await button.click(); // Expect the page to contain the updated counter text. await expect(main).toContainText("Server said: Hello from the server!"); diff --git a/packages/playwright-tests/nested-suspense-no-js.spec.js b/packages/playwright-tests/nested-suspense-no-js.spec.js index c76eab165..f4240b0ea 100644 --- a/packages/playwright-tests/nested-suspense-no-js.spec.js +++ b/packages/playwright-tests/nested-suspense-no-js.spec.js @@ -4,9 +4,23 @@ const { test, expect } = require("@playwright/test"); test.use({ javaScriptEnabled: false }); test("text appears in the body without javascript", async ({ page }) => { + await page.goto("http://localhost:5050", { waitUntil: "commit" }); + // Wait for the page to finish building. Reload until it's ready + for (let i = 0; i < 50; i++) { + // If the page doesn't contain #building or "Backend connection failed", we're ready + let building_count = await page.locator("#building").count(); + building_count += await page + .locator("body", { hasText: "backend connection failed" }) + .count(); + if (building_count === 0) { + break; + } + await page.waitForTimeout(1000); + await page.goto("http://localhost:5050", { waitUntil: "commit" }); + } // If we wait until the whole page loads, the content of the site should still be in the body even if javascript is disabled // It will not be visible, and may not be in the right order/location, but SEO should still work - await page.goto("http://localhost:5050"); + await page.waitForLoadState("load"); const body = page.locator("body"); const textExpected = [ diff --git a/packages/playwright-tests/nested-suspense.spec.js b/packages/playwright-tests/nested-suspense.spec.js index d3d9f7607..7b08bd10c 100644 --- a/packages/playwright-tests/nested-suspense.spec.js +++ b/packages/playwright-tests/nested-suspense.spec.js @@ -1,7 +1,11 @@ // @ts-check const { test, expect } = require("@playwright/test"); +const { timeout } = require("./playwright.config"); test("nested suspense resolves", async ({ page }) => { + // Wait for the dev server to reload + await page.goto("http://localhost:5050"); + // Then wait for the page to start loading await page.goto("http://localhost:5050", { waitUntil: "commit" }); // On the client, we should see some loading text diff --git a/packages/playwright-tests/playwright.config.js b/packages/playwright-tests/playwright.config.js index 79993c0c9..9efa89c8d 100644 --- a/packages/playwright-tests/playwright.config.js +++ b/packages/playwright-tests/playwright.config.js @@ -30,8 +30,12 @@ module.exports = defineConfig({ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: "on-first-retry", + // Increase the timeout for navigations to give dx time to build the project + navigationTimeout: 50 * 60 * 1000, }, + timeout: 50 * 60 * 1000, + /* Configure projects for major browsers */ projects: [ { diff --git a/packages/playwright-tests/static-generation/src/main.rs b/packages/playwright-tests/static-generation/src/main.rs index b868e8970..06a98c221 100644 --- a/packages/playwright-tests/static-generation/src/main.rs +++ b/packages/playwright-tests/static-generation/src/main.rs @@ -18,7 +18,7 @@ fn app() -> Element { rsx! { h1 { "hello axum! {count}" } button { class: "increment-button", onclick: move |_| count += 1, "Increment" } - "Server said: {server_data:?}" + "Server said: {server_data().unwrap():?}" } } diff --git a/packages/playwright-tests/suspense-carousel.spec.js b/packages/playwright-tests/suspense-carousel.spec.js index a09327e3a..cb147f2c6 100644 --- a/packages/playwright-tests/suspense-carousel.spec.js +++ b/packages/playwright-tests/suspense-carousel.spec.js @@ -2,6 +2,9 @@ const { test, expect } = require("@playwright/test"); test("suspense resolves on server", async ({ page }) => { + // Wait for the dev server to reload + await page.goto("http://localhost:4040"); + // Then wait for the page to start loading await page.goto("http://localhost:4040", { waitUntil: "commit" }); // On the client, we should see some loading text diff --git a/packages/rsx/src/element.rs b/packages/rsx/src/element.rs index d8583100e..6f1b3686c 100644 --- a/packages/rsx/src/element.rs +++ b/packages/rsx/src/element.rs @@ -558,6 +558,7 @@ fn merges_attributes() { /// - merging two expressions together /// - merging two literals together /// - merging a literal and an expression together +/// /// etc /// /// We really only want to merge formatted things together diff --git a/packages/ssr/src/lib.rs b/packages/ssr/src/lib.rs index 1f62ee9ae..7d666dab8 100644 --- a/packages/ssr/src/lib.rs +++ b/packages/ssr/src/lib.rs @@ -17,14 +17,7 @@ pub use crate::renderer::Renderer; /// /// For advanced rendering, create a new `SsrRender`. pub fn render_element(element: Element) -> String { - fn lazy_app(props: Element) -> Element { - props - } - - let mut dom = VirtualDom::new_with_props(lazy_app, element); - dom.rebuild_in_place(); - - Renderer::new().render(&dom) + Renderer::new().render_element(element) } /// A convenience function to render an existing VirtualDom to a string diff --git a/packages/ssr/src/renderer.rs b/packages/ssr/src/renderer.rs index 70d387b23..528d7782b 100644 --- a/packages/ssr/src/renderer.rs +++ b/packages/ssr/src/renderer.rs @@ -62,6 +62,27 @@ impl Renderer { self.render_scope(buf, dom, ScopeId::ROOT) } + /// Render an element to a string + pub fn render_element(&mut self, element: Element) -> String { + let mut buf = String::new(); + self.render_element_to(&mut buf, element).unwrap(); + buf + } + + /// Render an element to the buffer + pub fn render_element_to( + &mut self, + buf: &mut W, + element: Element, + ) -> std::fmt::Result { + fn lazy_app(props: Element) -> Element { + props + } + let mut dom = VirtualDom::new_with_props(lazy_app, element); + dom.rebuild_in_place(); + self.render_to(buf, &dom) + } + /// Reset the renderer hydration state pub fn reset_hydration(&mut self) { self.dynamic_node_id = 0; diff --git a/packages/static-generation/src/launch.rs b/packages/static-generation/src/launch.rs index dc8838cea..30aceae7a 100644 --- a/packages/static-generation/src/launch.rs +++ b/packages/static-generation/src/launch.rs @@ -42,11 +42,22 @@ pub fn launch( // Serve the program if we are running with cargo if std::env::var_os("CARGO").is_some() || std::env::var_os("DIOXUS_ACTIVE").is_some() { + // Get the address the server should run on. If the CLI is running, the CLI proxies static generation into the main address + // and we use the generated address the CLI gives us + let cli_args = dioxus_cli_config::RuntimeCLIArguments::from_cli(); + let address = cli_args + .as_ref() + .map(|args| args.fullstack_address().address()) + .unwrap_or_else(|| std::net::SocketAddr::from(([127, 0, 0, 1], 8080))); + + // Point the user to the CLI address if the CLI is running or the fullstack address if not + let serve_address = cli_args + .map(|args| args.cli_address()) + .unwrap_or_else(|| address); println!( - "Serving static files from {} at http://127.0.0.1:8080", + "Serving static files from {} at http://{serve_address}", path.display() ); - let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080)); let mut serve_dir = ServeDir::new(path.clone()).call_fallback_on_method_not_allowed(true); @@ -66,7 +77,7 @@ pub fn launch( }))) }; - let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); + let listener = tokio::net::TcpListener::bind(address).await.unwrap(); axum::serve(listener, router.into_make_service()) .await .unwrap(); diff --git a/packages/static-generation/src/ssg.rs b/packages/static-generation/src/ssg.rs index 4e9b0b9b3..20b1af53a 100644 --- a/packages/static-generation/src/ssg.rs +++ b/packages/static-generation/src/ssg.rs @@ -111,6 +111,8 @@ async fn prerender_route( let context = server_context_for_route(&route); let wrapper = config.fullstack_template(); let mut virtual_dom = VirtualDom::new(app); + let document = std::rc::Rc::new(dioxus_fullstack::document::ServerDocument::default()); + virtual_dom.provide_root_context(document.clone() as std::rc::Rc); with_server_context(context.clone(), || { tokio::task::block_in_place(|| virtual_dom.rebuild_in_place()); }); @@ -119,10 +121,11 @@ async fn prerender_route( let mut wrapped = String::new(); // Render everything before the body - wrapper.render_before_body(&mut wrapped)?; + wrapper.render_head(&mut wrapped, &virtual_dom)?; renderer.render_to(&mut wrapped, &virtual_dom)?; + wrapper.render_after_main(&mut wrapped, &virtual_dom)?; wrapper.render_after_body(&mut wrapped)?; cache.cache(route, wrapped) diff --git a/packages/web/src/document.rs b/packages/web/src/document.rs index 38091e04d..ce8828a5e 100644 --- a/packages/web/src/document.rs +++ b/packages/web/src/document.rs @@ -25,6 +25,10 @@ impl Document for WebDocument { fn new_evaluator(&self, js: String) -> GenerationalBox> { WebEvaluator::create(js) } + + fn as_any(&self) -> &dyn std::any::Any { + self + } } /// Required to avoid blocking the Rust WASM thread. diff --git a/packages/web/src/launch.rs b/packages/web/src/launch.rs index 0d0383d49..4cc421e9d 100644 --- a/packages/web/src/launch.rs +++ b/packages/web/src/launch.rs @@ -30,5 +30,5 @@ pub fn launch_virtual_dom(vdom: VirtualDom, platform_config: Config) { /// Launch the web application with the given root component and config pub fn launch_cfg(root: fn() -> Element, platform_config: Config) { - launch_virtual_dom(VirtualDom::new(root), platform_config) + launch(root, Vec::new(), platform_config); }