mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-26 14:10:20 +00:00
Fix playwright tests (#2695)
This commit is contained in:
parent
9479d2376d
commit
bd484842bd
28 changed files with 477 additions and 330 deletions
|
@ -117,6 +117,7 @@ impl BuildResult {
|
||||||
&self,
|
&self,
|
||||||
serve: &ServeArguments,
|
serve: &ServeArguments,
|
||||||
fullstack_address: Option<SocketAddr>,
|
fullstack_address: Option<SocketAddr>,
|
||||||
|
workspace: &std::path::Path,
|
||||||
) -> std::io::Result<Option<Child>> {
|
) -> std::io::Result<Option<Child>> {
|
||||||
if self.web {
|
if self.web {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
|
@ -124,12 +125,8 @@ impl BuildResult {
|
||||||
|
|
||||||
let arguments = RuntimeCLIArguments::new(serve.address.address(), fullstack_address);
|
let arguments = RuntimeCLIArguments::new(serve.address.address(), fullstack_address);
|
||||||
let executable = self.executable.canonicalize()?;
|
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(
|
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
|
// When building the fullstack server, we need to forward the serve arguments (like port) to the fullstack server through env vars
|
||||||
.env(
|
.env(
|
||||||
dioxus_cli_config::__private::SERVE_ENV,
|
dioxus_cli_config::__private::SERVE_ENV,
|
||||||
|
@ -138,7 +135,7 @@ impl BuildResult {
|
||||||
.stderr(Stdio::piped())
|
.stderr(Stdio::piped())
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.kill_on_drop(true)
|
.kill_on_drop(true)
|
||||||
.current_dir(workspace_folder)
|
.current_dir(workspace)
|
||||||
.spawn()?,
|
.spawn()?,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
|
@ -117,7 +117,7 @@ pub async fn serve_all(serve: Serve, dioxus_crate: DioxusCrate) -> Result<()> {
|
||||||
|
|
||||||
// If we have a build result, open it
|
// If we have a build result, open it
|
||||||
for build_result in results.iter() {
|
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 {
|
match child {
|
||||||
Ok(Some(child_proc)) => builder.children.push((build_result.platform,child_proc)),
|
Ok(Some(child_proc)) => builder.children.push((build_result.platform,child_proc)),
|
||||||
Err(_e) => break,
|
Err(_e) => break,
|
||||||
|
|
|
@ -21,6 +21,7 @@ use axum_server::tls_rustls::RustlsConfig;
|
||||||
use dioxus_cli_config::{Platform, WebHttpsConfig};
|
use dioxus_cli_config::{Platform, WebHttpsConfig};
|
||||||
use dioxus_hot_reload::{DevserverMsg, HotReloadMsg};
|
use dioxus_hot_reload::{DevserverMsg, HotReloadMsg};
|
||||||
use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender};
|
use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender};
|
||||||
|
use futures_util::stream;
|
||||||
use futures_util::{stream::FuturesUnordered, StreamExt};
|
use futures_util::{stream::FuturesUnordered, StreamExt};
|
||||||
use hyper::header::ACCEPT;
|
use hyper::header::ACCEPT;
|
||||||
use hyper::HeaderMap;
|
use hyper::HeaderMap;
|
||||||
|
@ -577,7 +578,9 @@ pub(crate) fn open_browser(base_path: Option<String>, address: SocketAddr, https
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_available_port(address: IpAddr) -> Option<u16> {
|
fn get_available_port(address: IpAddr) -> Option<u16> {
|
||||||
(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
|
/// 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");
|
let html = include_str!("../../assets/loading.html");
|
||||||
return axum::response::Response::builder()
|
return axum::response::Response::builder()
|
||||||
.status(StatusCode::OK)
|
.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();
|
.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -276,7 +276,6 @@ impl SuspenseBoundaryProps {
|
||||||
to: Option<&mut M>,
|
to: Option<&mut M>,
|
||||||
) -> usize {
|
) -> usize {
|
||||||
let mut scope_id = ScopeId(dom.mounts[mount.0].mounted_dynamic_nodes[idx]);
|
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 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() {
|
if scope_id.is_placeholder() {
|
||||||
{
|
{
|
||||||
|
@ -300,78 +299,80 @@ impl SuspenseBoundaryProps {
|
||||||
// Store the scope id for the next render
|
// Store the scope id for the next render
|
||||||
dom.mounts[mount.0].mounted_dynamic_nodes[idx] = scope_id.0;
|
dom.mounts[mount.0].mounted_dynamic_nodes[idx] = scope_id.0;
|
||||||
}
|
}
|
||||||
|
dom.runtime.clone().with_scope_on_stack(scope_id, || {
|
||||||
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)
|
|
||||||
});
|
|
||||||
|
|
||||||
let scope_state = &mut dom.scopes[scope_id.0];
|
let scope_state = &mut dom.scopes[scope_id.0];
|
||||||
scope_state.last_rendered_node = Some(node);
|
let props = Self::downcast_from_props(&mut *scope_state.props).unwrap();
|
||||||
|
|
||||||
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 =
|
let suspense_context =
|
||||||
SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id)
|
SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id)
|
||||||
.unwrap();
|
.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
|
||||||
};
|
})
|
||||||
|
|
||||||
nodes_created
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
|
@ -385,65 +386,69 @@ impl SuspenseBoundaryProps {
|
||||||
only_write_templates: impl FnOnce(&mut M),
|
only_write_templates: impl FnOnce(&mut M),
|
||||||
replace_with: usize,
|
replace_with: usize,
|
||||||
) {
|
) {
|
||||||
let _runtime = RuntimeGuard::new(dom.runtime());
|
dom.runtime.clone().with_scope_on_stack(scope_id, || {
|
||||||
let Some(scope_state) = dom.scopes.get_mut(scope_id.0) else {
|
let _runtime = RuntimeGuard::new(dom.runtime());
|
||||||
return;
|
let Some(scope_state) = dom.scopes.get_mut(scope_id.0) else {
|
||||||
};
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
// Reset the suspense context
|
// Reset the suspense context
|
||||||
let suspense_context = scope_state
|
let suspense_context = scope_state
|
||||||
.state()
|
.state()
|
||||||
.suspense_location()
|
.suspense_location()
|
||||||
.suspense_context()
|
.suspense_context()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.clone();
|
.clone();
|
||||||
suspense_context.inner.suspended_tasks.borrow_mut().clear();
|
suspense_context.inner.suspended_tasks.borrow_mut().clear();
|
||||||
|
|
||||||
// Get the parent of the suspense boundary to later create children with the right 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 currently_rendered = scope_state.last_rendered_node.as_ref().unwrap().clone();
|
||||||
let mount = currently_rendered.mount.get();
|
let mount = currently_rendered.mount.get();
|
||||||
let parent = dom
|
let parent = dom
|
||||||
.mounts
|
.mounts
|
||||||
.get(mount.0)
|
.get(mount.0)
|
||||||
.expect("suspense placeholder is not mounted")
|
.expect("suspense placeholder is not mounted")
|
||||||
.parent;
|
.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
|
// Unmount any children to reset any scopes under this suspense boundary
|
||||||
let children = props
|
let children = props
|
||||||
.children
|
.children
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|node| node.clone_mounted())
|
.map(|node| node.clone_mounted())
|
||||||
.map_err(Clone::clone);
|
.map_err(Clone::clone);
|
||||||
let suspense_context =
|
let suspense_context =
|
||||||
SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id).unwrap();
|
SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id)
|
||||||
let suspended = suspense_context.suspended_nodes();
|
.unwrap();
|
||||||
if let Some(node) = suspended {
|
let suspended = suspense_context.suspended_nodes();
|
||||||
node.remove_node(&mut *dom, None::<&mut M>, None);
|
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));
|
// Replace the rendered nodes with resolved nodes
|
||||||
|
currently_rendered.remove_node(&mut *dom, Some(to), Some(replace_with));
|
||||||
|
|
||||||
// Switch to only writing templates
|
// Switch to only writing templates
|
||||||
only_write_templates(to);
|
only_write_templates(to);
|
||||||
|
|
||||||
let children = RenderReturn { node: children };
|
let children = RenderReturn { node: children };
|
||||||
children.mount.take();
|
children.mount.take();
|
||||||
|
|
||||||
// First always render the children in the background. Rendering the children may cause this boundary to suspend
|
// First always render the children in the background. Rendering the children may cause this boundary to suspend
|
||||||
suspense_context.under_suspense_boundary(&dom.runtime(), || {
|
suspense_context.under_suspense_boundary(&dom.runtime(), || {
|
||||||
children.create(dom, parent, Some(to));
|
children.create(dom, parent, Some(to));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Store the (now mounted) children back into the scope state
|
// Store the (now mounted) children back into the scope state
|
||||||
let scope_state = &mut dom.scopes[scope_id.0];
|
let scope_state = &mut dom.scopes[scope_id.0];
|
||||||
let props = Self::downcast_from_props(&mut *scope_state.props).unwrap();
|
let props = Self::downcast_from_props(&mut *scope_state.props).unwrap();
|
||||||
props.children = children.clone().node;
|
props.children = children.clone().node;
|
||||||
scope_state.last_rendered_node = Some(children);
|
scope_state.last_rendered_node = Some(children);
|
||||||
let suspense_context =
|
let suspense_context =
|
||||||
SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id).unwrap();
|
SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id)
|
||||||
suspense_context.take_suspended_nodes();
|
.unwrap();
|
||||||
|
suspense_context.take_suspended_nodes();
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn diff<M: WriteMutations>(
|
pub(crate) fn diff<M: WriteMutations>(
|
||||||
|
@ -451,127 +456,140 @@ impl SuspenseBoundaryProps {
|
||||||
dom: &mut VirtualDom,
|
dom: &mut VirtualDom,
|
||||||
to: Option<&mut M>,
|
to: Option<&mut M>,
|
||||||
) {
|
) {
|
||||||
let scope = &mut dom.scopes[scope_id.0];
|
dom.runtime.clone().with_scope_on_stack(scope_id, || {
|
||||||
let myself = Self::downcast_from_props(&mut *scope.props)
|
let scope = &mut dom.scopes[scope_id.0];
|
||||||
.unwrap()
|
let myself = Self::downcast_from_props(&mut *scope.props)
|
||||||
.clone();
|
.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 {
|
let Self {
|
||||||
fallback, children, ..
|
fallback, children, ..
|
||||||
} = myself;
|
} = myself;
|
||||||
|
|
||||||
let suspense_context = scope.state().suspense_boundary().unwrap().clone();
|
let suspense_context = scope.state().suspense_boundary().unwrap().clone();
|
||||||
let suspended_nodes = suspense_context.suspended_nodes();
|
let suspended_nodes = suspense_context.suspended_nodes();
|
||||||
let suspended = !suspense_context.suspended_futures().is_empty();
|
let suspended = !suspense_context.suspended_futures().is_empty();
|
||||||
match (suspended_nodes, suspended) {
|
match (suspended_nodes, suspended) {
|
||||||
// We already have suspended nodes that still need to be suspended
|
// We already have suspended nodes that still need to be suspended
|
||||||
// Just diff the normal and suspended nodes
|
// Just diff the normal and suspended nodes
|
||||||
(Some(suspended_nodes), true) => {
|
(Some(suspended_nodes), true) => {
|
||||||
let new_suspended_nodes: VNode = RenderReturn { node: children }.into();
|
let new_suspended_nodes: VNode = RenderReturn { node: children }.into();
|
||||||
|
|
||||||
// Diff the placeholder nodes in the dom
|
// Diff the placeholder nodes in the dom
|
||||||
let new_placeholder =
|
let new_placeholder =
|
||||||
suspense_context.in_suspense_placeholder(&dom.runtime(), || {
|
suspense_context.in_suspense_placeholder(&dom.runtime(), || {
|
||||||
let old_placeholder = last_rendered_node;
|
let old_placeholder = last_rendered_node;
|
||||||
let new_placeholder = RenderReturn {
|
let new_placeholder = RenderReturn {
|
||||||
node: fallback.call(suspense_context.clone()),
|
node: fallback.call(suspense_context.clone()),
|
||||||
};
|
};
|
||||||
|
|
||||||
old_placeholder.diff_node(&new_placeholder, dom, to);
|
old_placeholder.diff_node(&new_placeholder, dom, to);
|
||||||
new_placeholder
|
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
|
let suspense_context = SuspenseContext::downcast_suspense_boundary_from_scope(
|
||||||
dom.scopes[scope_id.0].last_rendered_node = Some(new_placeholder);
|
&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(), || {
|
||||||
suspense_context.under_suspense_boundary(&dom.runtime(), || {
|
old_children.diff_node(&new_children, dom, to);
|
||||||
suspended_nodes.diff_node(&new_suspended_nodes, dom, None::<&mut M>);
|
});
|
||||||
});
|
|
||||||
|
|
||||||
let suspense_context =
|
// Set the last rendered node to the new children
|
||||||
SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id)
|
dom.scopes[scope_id.0].last_rendered_node = Some(new_children);
|
||||||
.unwrap();
|
}
|
||||||
suspense_context.set_suspended_nodes(new_suspended_nodes);
|
// We have no suspended nodes, but we just became suspended. Move the children to the background
|
||||||
}
|
(None, true) => {
|
||||||
// We have no suspended nodes, and we are not suspended. Just diff the children like normal
|
let old_children = last_rendered_node;
|
||||||
(None, false) => {
|
let new_children: VNode = RenderReturn { node: children }.into();
|
||||||
let old_children = last_rendered_node;
|
|
||||||
let new_children = RenderReturn { node: children };
|
|
||||||
|
|
||||||
suspense_context.under_suspense_boundary(&dom.runtime(), || {
|
let new_placeholder = RenderReturn {
|
||||||
old_children.diff_node(&new_children, dom, to);
|
node: fallback.call(suspense_context.clone()),
|
||||||
});
|
};
|
||||||
|
|
||||||
// Set the last rendered node to the new children
|
// Move the children to the background
|
||||||
dom.scopes[scope_id.0].last_rendered_node = Some(new_children);
|
let mount = old_children.mount.get();
|
||||||
}
|
|
||||||
// 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();
|
|
||||||
let mount = dom.mounts.get(mount.0).expect("mount should exist");
|
let mount = dom.mounts.get(mount.0).expect("mount should exist");
|
||||||
let parent = mount.parent;
|
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
|
suspense_context.in_suspense_placeholder(&dom.runtime(), || {
|
||||||
dom.scopes[scope_id.0].last_rendered_node = Some(new_children);
|
old_children.move_node_to_background(
|
||||||
|
std::slice::from_ref(&*new_placeholder),
|
||||||
|
parent,
|
||||||
|
dom,
|
||||||
|
to,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
let suspense_context =
|
// Then diff the new children in the background
|
||||||
SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id)
|
suspense_context.under_suspense_boundary(&dom.runtime(), || {
|
||||||
.unwrap();
|
old_children.diff_node(&new_children, dom, None::<&mut M>);
|
||||||
suspense_context.take_suspended_nodes();
|
});
|
||||||
|
|
||||||
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,10 @@ impl Document for DesktopDocument {
|
||||||
fn set_title(&self, title: String) {
|
fn set_title(&self, title: String) {
|
||||||
self.desktop_ctx.window.set_title(&title);
|
self.desktop_ctx.window.set_title(&title);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn as_any(&self) -> &dyn std::any::Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents a desktop-target's JavaScript evaluator.
|
/// Represents a desktop-target's JavaScript evaluator.
|
||||||
|
|
|
@ -104,7 +104,7 @@ server = [
|
||||||
"dep:parking_lot",
|
"dep:parking_lot",
|
||||||
"dioxus-interpreter-js",
|
"dioxus-interpreter-js",
|
||||||
"dep:clap",
|
"dep:clap",
|
||||||
"dioxus-cli-config/read-from-args"
|
"dioxus-cli-config/read-from-args",
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
|
//! This module contains the document providers for the fullstack platform.
|
||||||
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
pub(crate) mod 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;
|
pub(crate) mod web;
|
||||||
|
|
|
@ -7,6 +7,10 @@ use std::cell::RefCell;
|
||||||
use dioxus_lib::{html::document::*, prelude::*};
|
use dioxus_lib::{html::document::*, prelude::*};
|
||||||
use dioxus_ssr::Renderer;
|
use dioxus_ssr::Renderer;
|
||||||
use generational_box::GenerationalBox;
|
use generational_box::GenerationalBox;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use parking_lot::RwLock;
|
||||||
|
|
||||||
|
static RENDERER: Lazy<RwLock<Renderer>> = Lazy::new(|| RwLock::new(Renderer::new()));
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct ServerDocumentInner {
|
struct ServerDocumentInner {
|
||||||
|
@ -19,35 +23,27 @@ struct ServerDocumentInner {
|
||||||
|
|
||||||
/// A Document provider that collects all contents injected into the head for SSR rendering.
|
/// A Document provider that collects all contents injected into the head for SSR rendering.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub(crate) struct ServerDocument(RefCell<ServerDocumentInner>);
|
pub struct ServerDocument(RefCell<ServerDocumentInner>);
|
||||||
|
|
||||||
impl ServerDocument {
|
impl ServerDocument {
|
||||||
pub(crate) fn render(
|
pub(crate) fn title(&self) -> Option<String> {
|
||||||
&self,
|
let myself = self.0.borrow();
|
||||||
to: &mut impl std::fmt::Write,
|
myself.title.as_ref().map(|title| {
|
||||||
renderer: &mut Renderer,
|
RENDERER
|
||||||
) -> std::fmt::Result {
|
.write()
|
||||||
fn lazy_app(props: Element) -> Element {
|
.render_element(rsx! { title { "{title}" } })
|
||||||
props
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn render(&self, to: &mut impl std::fmt::Write) -> std::fmt::Result {
|
||||||
let myself = self.0.borrow();
|
let myself = self.0.borrow();
|
||||||
let element = rsx! {
|
let element = rsx! {
|
||||||
if let Some(title) = myself.title.as_ref() {
|
|
||||||
title { title: "{title}" }
|
|
||||||
}
|
|
||||||
{myself.meta.iter().map(|m| rsx! { {m} })}
|
{myself.meta.iter().map(|m| rsx! { {m} })}
|
||||||
{myself.link.iter().map(|l| rsx! { {l} })}
|
{myself.link.iter().map(|l| rsx! { {l} })}
|
||||||
{myself.script.iter().map(|s| rsx! { {s} })}
|
{myself.script.iter().map(|s| rsx! { {s} })}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut dom = VirtualDom::new_with_props(lazy_app, element);
|
RENDERER.write().render_element_to(to, 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;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -65,8 +61,12 @@ impl ServerDocument {
|
||||||
/// Write the head element into the serialized context for hydration
|
/// 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
|
/// We write true if the head element was written to the DOM during server side rendering
|
||||||
pub(crate) fn serialize_for_hydration(&self) {
|
pub(crate) fn serialize_for_hydration(&self) {
|
||||||
let serialize = crate::html_storage::serialize_context();
|
// We only serialize the head elements if the web document feature is enabled
|
||||||
serialize.push(&!self.0.borrow().streaming);
|
#[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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,4 +55,8 @@ impl Document for FullstackWebDocument {
|
||||||
}
|
}
|
||||||
WebDocument.create_link(props);
|
WebDocument.create_link(props);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn as_any(&self) -> &dyn std::any::Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,8 +62,9 @@ pub fn launch(
|
||||||
#[cfg(feature = "document")]
|
#[cfg(feature = "document")]
|
||||||
let factory = move || {
|
let factory = move || {
|
||||||
let mut vdom = factory();
|
let mut vdom = factory();
|
||||||
vdom.provide_root_context(std::rc::Rc::new(crate::document::web::FullstackWebDocument)
|
let document = std::rc::Rc::new(crate::document::web::FullstackWebDocument)
|
||||||
as std::rc::Rc<dyn dioxus_lib::prelude::document::Document>);
|
as std::rc::Rc<dyn dioxus_lib::prelude::document::Document>;
|
||||||
|
vdom.provide_root_context(document);
|
||||||
vdom
|
vdom
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -18,8 +18,7 @@ pub mod launch;
|
||||||
|
|
||||||
pub use config::*;
|
pub use config::*;
|
||||||
|
|
||||||
#[cfg(feature = "document")]
|
pub mod document;
|
||||||
mod document;
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
mod render;
|
mod render;
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
|
|
|
@ -7,9 +7,9 @@ use dioxus_ssr::{
|
||||||
};
|
};
|
||||||
use futures_channel::mpsc::Sender;
|
use futures_channel::mpsc::Sender;
|
||||||
use futures_util::{Stream, StreamExt};
|
use futures_util::{Stream, StreamExt};
|
||||||
|
use std::sync::Arc;
|
||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
use std::{collections::HashMap, future::Future};
|
use std::{collections::HashMap, future::Future};
|
||||||
use std::{fmt::Write, sync::Arc};
|
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
@ -160,9 +160,7 @@ impl SsrRendererPool {
|
||||||
|
|
||||||
let join_handle = spawn_platform(move || async move {
|
let join_handle = spawn_platform(move || async move {
|
||||||
let mut virtual_dom = virtual_dom_factory();
|
let mut virtual_dom = virtual_dom_factory();
|
||||||
#[cfg(feature = "document")]
|
|
||||||
let document = std::rc::Rc::new(crate::document::server::ServerDocument::default());
|
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<dyn Document>);
|
virtual_dom.provide_root_context(document.clone() as std::rc::Rc<dyn Document>);
|
||||||
|
|
||||||
// poll the future, which may call server_context()
|
// poll the future, which may call server_context()
|
||||||
|
@ -171,34 +169,11 @@ impl SsrRendererPool {
|
||||||
|
|
||||||
let mut pre_body = String::new();
|
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));
|
_ = into.start_send(Err(err));
|
||||||
return;
|
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, "<script>{INITIALIZE_STREAMING_JS}</script>") {
|
|
||||||
_ = into.start_send(Err(
|
|
||||||
dioxus_ssr::incremental::IncrementalRendererError::RenderError(err),
|
|
||||||
));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let stream = Arc::new(StreamingRenderer::new(pre_body, into));
|
let stream = Arc::new(StreamingRenderer::new(pre_body, into));
|
||||||
let scope_to_mount_mapping = Arc::new(RwLock::new(HashMap::new()));
|
let scope_to_mount_mapping = Arc::new(RwLock::new(HashMap::new()));
|
||||||
|
|
||||||
|
@ -237,15 +212,8 @@ impl SsrRendererPool {
|
||||||
// Render the initial frame with loading placeholders
|
// Render the initial frame with loading placeholders
|
||||||
let mut initial_frame = renderer.render(&virtual_dom);
|
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#"<script>window.initial_dioxus_hydration_data="{resolved_data}";</script>"#,
|
|
||||||
));
|
|
||||||
|
|
||||||
// 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.
|
// 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);
|
throw_error!(err);
|
||||||
}
|
}
|
||||||
stream.render(initial_frame);
|
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 incremental rendering is enabled, add the new render to the cache without the streaming bits
|
||||||
if let Some(incremental) = &self.incremental_cache {
|
if let Some(incremental) = &self.incremental_cache {
|
||||||
let mut cached_render = String::new();
|
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);
|
throw_error!(err);
|
||||||
}
|
}
|
||||||
cached_render.push_str(&post_streaming);
|
cached_render.push_str(&post_streaming);
|
||||||
|
@ -401,16 +369,48 @@ impl FullstackHTMLTemplate {
|
||||||
pub fn render_head<R: std::fmt::Write>(
|
pub fn render_head<R: std::fmt::Write>(
|
||||||
&self,
|
&self,
|
||||||
to: &mut R,
|
to: &mut R,
|
||||||
|
virtual_dom: &VirtualDom,
|
||||||
) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> {
|
) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> {
|
||||||
let ServeConfig { index, .. } = &self.cfg;
|
let ServeConfig { index, .. } = &self.cfg;
|
||||||
|
|
||||||
to.write_str(&index.head)?;
|
let title = {
|
||||||
|
let document: Option<std::rc::Rc<dyn dioxus_lib::prelude::document::Document>> =
|
||||||
|
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<std::rc::Rc<dyn dioxus_lib::prelude::document::Document>> =
|
||||||
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render any content before the body of the page.
|
/// Render any content before the body of the page.
|
||||||
pub fn render_before_body<R: std::fmt::Write>(
|
fn render_before_body<R: std::fmt::Write>(
|
||||||
&self,
|
&self,
|
||||||
to: &mut R,
|
to: &mut R,
|
||||||
) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> {
|
) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> {
|
||||||
|
@ -418,6 +418,8 @@ impl FullstackHTMLTemplate {
|
||||||
|
|
||||||
to.write_str(&index.close_head)?;
|
to.write_str(&index.close_head)?;
|
||||||
|
|
||||||
|
write!(to, "<script>{INITIALIZE_STREAMING_JS}</script>")?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -425,9 +427,17 @@ impl FullstackHTMLTemplate {
|
||||||
pub fn render_after_main<R: std::fmt::Write>(
|
pub fn render_after_main<R: std::fmt::Write>(
|
||||||
&self,
|
&self,
|
||||||
to: &mut R,
|
to: &mut R,
|
||||||
|
virtual_dom: &VirtualDom,
|
||||||
) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> {
|
) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> {
|
||||||
let ServeConfig { index, .. } = &self.cfg;
|
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#"<script>window.initial_dioxus_hydration_data="{resolved_data}";</script>"#,
|
||||||
|
)?;
|
||||||
to.write_str(&index.post_main)?;
|
to.write_str(&index.post_main)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -444,6 +454,21 @@ impl FullstackHTMLTemplate {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wrap a body in the template
|
||||||
|
pub fn wrap_body<R: std::fmt::Write>(
|
||||||
|
&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 {
|
fn pre_renderer() -> Renderer {
|
||||||
|
|
|
@ -118,8 +118,23 @@ fn load_index_html(contents: String, root_id: &'static str) -> IndexHtml {
|
||||||
panic!("Failed to find closing </body> tag after id=\"{root_id}\" in index.html.")
|
panic!("Failed to find closing </body> 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("<title>") {
|
||||||
|
let (new_title, new_head_after_title) = new_title
|
||||||
|
.split_once("</title>")
|
||||||
|
.expect("Failed to find closing </title> tag after <title> in index.html.");
|
||||||
|
title = format!("<title>{new_title}</title>");
|
||||||
|
head_before_title = new_head_before_title.to_string();
|
||||||
|
head_after_title = new_head_after_title.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
IndexHtml {
|
IndexHtml {
|
||||||
head,
|
head_before_title,
|
||||||
|
head_after_title,
|
||||||
|
title,
|
||||||
close_head,
|
close_head,
|
||||||
post_main: post_main.to_string(),
|
post_main: post_main.to_string(),
|
||||||
after_closing_body_tag: "</body>".to_string() + after_closing_body_tag,
|
after_closing_body_tag: "</body>".to_string() + after_closing_body_tag,
|
||||||
|
@ -128,7 +143,9 @@ fn load_index_html(contents: String, root_id: &'static str) -> IndexHtml {
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) struct IndexHtml {
|
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) close_head: String,
|
||||||
pub(crate) post_main: String,
|
pub(crate) post_main: String,
|
||||||
pub(crate) after_closing_body_tag: String,
|
pub(crate) after_closing_body_tag: String,
|
||||||
|
|
|
@ -51,29 +51,36 @@ pub trait Document {
|
||||||
self.new_evaluator(format!("document.title = {title:?};"));
|
self.new_evaluator(format!("document.title = {title:?};"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a new meta tag
|
||||||
fn create_meta(&self, props: MetaProps) {
|
fn create_meta(&self, props: MetaProps) {
|
||||||
let attributes = props.attributes();
|
let attributes = props.attributes();
|
||||||
let js = create_element_in_head("meta", &attributes, None);
|
let js = create_element_in_head("meta", &attributes, None);
|
||||||
self.new_evaluator(js);
|
self.new_evaluator(js);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a new script tag
|
||||||
fn create_script(&self, props: ScriptProps) {
|
fn create_script(&self, props: ScriptProps) {
|
||||||
let attributes = props.attributes();
|
let attributes = props.attributes();
|
||||||
let js = create_element_in_head("script", &attributes, props.script_contents());
|
let js = create_element_in_head("script", &attributes, props.script_contents());
|
||||||
self.new_evaluator(js);
|
self.new_evaluator(js);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a new style tag
|
||||||
fn create_style(&self, props: StyleProps) {
|
fn create_style(&self, props: StyleProps) {
|
||||||
let attributes = props.attributes();
|
let attributes = props.attributes();
|
||||||
let js = create_element_in_head("style", &attributes, props.style_contents());
|
let js = create_element_in_head("style", &attributes, props.style_contents());
|
||||||
self.new_evaluator(js);
|
self.new_evaluator(js);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a new link tag
|
||||||
fn create_link(&self, props: head::LinkProps) {
|
fn create_link(&self, props: head::LinkProps) {
|
||||||
let attributes = props.attributes();
|
let attributes = props.attributes();
|
||||||
let js = create_element_in_head("link", &attributes, None);
|
let js = create_element_in_head("link", &attributes, None);
|
||||||
self.new_evaluator(js);
|
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
|
/// 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.");
|
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))
|
UnsyncStorage::owner().insert(Box::new(NoOpEvaluator))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn as_any(&self) -> &dyn std::any::Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct NoOpEvaluator;
|
struct NoOpEvaluator;
|
||||||
|
|
|
@ -21,6 +21,10 @@ impl Document for LiveviewDocument {
|
||||||
fn new_evaluator(&self, js: String) -> GenerationalBox<Box<dyn Evaluator>> {
|
fn new_evaluator(&self, js: String) -> GenerationalBox<Box<dyn Evaluator>> {
|
||||||
LiveviewEvaluator::create(self.query.clone(), js)
|
LiveviewEvaluator::create(self.query.clone(), js)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn as_any(&self) -> &dyn std::any::Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents a liveview-target's JavaScript evaluator.
|
/// Represents a liveview-target's JavaScript evaluator.
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
const { test, expect } = require("@playwright/test");
|
const { test, expect } = require("@playwright/test");
|
||||||
|
|
||||||
test("button click", async ({ page }) => {
|
test("hydration", async ({ page }) => {
|
||||||
await page.goto("http://localhost:3333");
|
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.
|
// Expect the page to contain the counter text.
|
||||||
const main = page.locator("#main");
|
|
||||||
await expect(main).toContainText("hello axum! 12345");
|
await expect(main).toContainText("hello axum! 12345");
|
||||||
// Expect the title to contain the counter text.
|
// Expect the title to contain the counter text.
|
||||||
await expect(page).toHaveTitle("hello axum! 12345");
|
await expect(page).toHaveTitle("hello axum! 12345");
|
||||||
|
@ -15,23 +17,14 @@ test("button click", async ({ page }) => {
|
||||||
let button = page.locator("button.increment-button");
|
let button = page.locator("button.increment-button");
|
||||||
await button.click();
|
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.
|
// Expect the page to contain the updated counter text.
|
||||||
await expect(main).toContainText("hello axum! 12346");
|
await expect(main).toContainText("hello axum! 12346");
|
||||||
// Expect the title to contain the updated counter text.
|
// Expect the title to contain the updated counter text.
|
||||||
await expect(page).toHaveTitle("hello axum! 12346");
|
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.
|
// Expect the page to contain the updated counter text.
|
||||||
await expect(main).toContainText("Server said: Hello from the server!");
|
await expect(main).toContainText("Server said: Hello from the server!");
|
||||||
|
|
|
@ -4,9 +4,23 @@ const { test, expect } = require("@playwright/test");
|
||||||
test.use({ javaScriptEnabled: false });
|
test.use({ javaScriptEnabled: false });
|
||||||
|
|
||||||
test("text appears in the body without javascript", async ({ page }) => {
|
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
|
// 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
|
// 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 body = page.locator("body");
|
||||||
const textExpected = [
|
const textExpected = [
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
const { test, expect } = require("@playwright/test");
|
const { test, expect } = require("@playwright/test");
|
||||||
|
const { timeout } = require("./playwright.config");
|
||||||
|
|
||||||
test("nested suspense resolves", async ({ page }) => {
|
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" });
|
await page.goto("http://localhost:5050", { waitUntil: "commit" });
|
||||||
|
|
||||||
// On the client, we should see some loading text
|
// On the client, we should see some loading text
|
||||||
|
|
|
@ -30,8 +30,12 @@ module.exports = defineConfig({
|
||||||
|
|
||||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
trace: "on-first-retry",
|
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 */
|
/* Configure projects for major browsers */
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
|
|
|
@ -18,7 +18,7 @@ fn app() -> Element {
|
||||||
rsx! {
|
rsx! {
|
||||||
h1 { "hello axum! {count}" }
|
h1 { "hello axum! {count}" }
|
||||||
button { class: "increment-button", onclick: move |_| count += 1, "Increment" }
|
button { class: "increment-button", onclick: move |_| count += 1, "Increment" }
|
||||||
"Server said: {server_data:?}"
|
"Server said: {server_data().unwrap():?}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,9 @@
|
||||||
const { test, expect } = require("@playwright/test");
|
const { test, expect } = require("@playwright/test");
|
||||||
|
|
||||||
test("suspense resolves on server", async ({ page }) => {
|
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" });
|
await page.goto("http://localhost:4040", { waitUntil: "commit" });
|
||||||
|
|
||||||
// On the client, we should see some loading text
|
// On the client, we should see some loading text
|
||||||
|
|
|
@ -558,6 +558,7 @@ fn merges_attributes() {
|
||||||
/// - merging two expressions together
|
/// - merging two expressions together
|
||||||
/// - merging two literals together
|
/// - merging two literals together
|
||||||
/// - merging a literal and an expression together
|
/// - merging a literal and an expression together
|
||||||
|
///
|
||||||
/// etc
|
/// etc
|
||||||
///
|
///
|
||||||
/// We really only want to merge formatted things together
|
/// We really only want to merge formatted things together
|
||||||
|
|
|
@ -17,14 +17,7 @@ pub use crate::renderer::Renderer;
|
||||||
///
|
///
|
||||||
/// For advanced rendering, create a new `SsrRender`.
|
/// For advanced rendering, create a new `SsrRender`.
|
||||||
pub fn render_element(element: Element) -> String {
|
pub fn render_element(element: Element) -> String {
|
||||||
fn lazy_app(props: Element) -> Element {
|
Renderer::new().render_element(element)
|
||||||
props
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut dom = VirtualDom::new_with_props(lazy_app, element);
|
|
||||||
dom.rebuild_in_place();
|
|
||||||
|
|
||||||
Renderer::new().render(&dom)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A convenience function to render an existing VirtualDom to a string
|
/// A convenience function to render an existing VirtualDom to a string
|
||||||
|
|
|
@ -62,6 +62,27 @@ impl Renderer {
|
||||||
self.render_scope(buf, dom, ScopeId::ROOT)
|
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<W: Write + ?Sized>(
|
||||||
|
&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
|
/// Reset the renderer hydration state
|
||||||
pub fn reset_hydration(&mut self) {
|
pub fn reset_hydration(&mut self) {
|
||||||
self.dynamic_node_id = 0;
|
self.dynamic_node_id = 0;
|
||||||
|
|
|
@ -42,11 +42,22 @@ pub fn launch(
|
||||||
|
|
||||||
// Serve the program if we are running with cargo
|
// 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() {
|
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!(
|
println!(
|
||||||
"Serving static files from {} at http://127.0.0.1:8080",
|
"Serving static files from {} at http://{serve_address}",
|
||||||
path.display()
|
path.display()
|
||||||
);
|
);
|
||||||
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
|
|
||||||
|
|
||||||
let mut serve_dir =
|
let mut serve_dir =
|
||||||
ServeDir::new(path.clone()).call_fallback_on_method_not_allowed(true);
|
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())
|
axum::serve(listener, router.into_make_service())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
@ -111,6 +111,8 @@ async fn prerender_route(
|
||||||
let context = server_context_for_route(&route);
|
let context = server_context_for_route(&route);
|
||||||
let wrapper = config.fullstack_template();
|
let wrapper = config.fullstack_template();
|
||||||
let mut virtual_dom = VirtualDom::new(app);
|
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<dyn Document>);
|
||||||
with_server_context(context.clone(), || {
|
with_server_context(context.clone(), || {
|
||||||
tokio::task::block_in_place(|| virtual_dom.rebuild_in_place());
|
tokio::task::block_in_place(|| virtual_dom.rebuild_in_place());
|
||||||
});
|
});
|
||||||
|
@ -119,10 +121,11 @@ async fn prerender_route(
|
||||||
let mut wrapped = String::new();
|
let mut wrapped = String::new();
|
||||||
|
|
||||||
// Render everything before the body
|
// 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)?;
|
renderer.render_to(&mut wrapped, &virtual_dom)?;
|
||||||
|
|
||||||
|
wrapper.render_after_main(&mut wrapped, &virtual_dom)?;
|
||||||
wrapper.render_after_body(&mut wrapped)?;
|
wrapper.render_after_body(&mut wrapped)?;
|
||||||
|
|
||||||
cache.cache(route, wrapped)
|
cache.cache(route, wrapped)
|
||||||
|
|
|
@ -25,6 +25,10 @@ impl Document for WebDocument {
|
||||||
fn new_evaluator(&self, js: String) -> GenerationalBox<Box<dyn Evaluator>> {
|
fn new_evaluator(&self, js: String) -> GenerationalBox<Box<dyn Evaluator>> {
|
||||||
WebEvaluator::create(js)
|
WebEvaluator::create(js)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn as_any(&self) -> &dyn std::any::Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Required to avoid blocking the Rust WASM thread.
|
/// Required to avoid blocking the Rust WASM thread.
|
||||||
|
|
|
@ -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
|
/// Launch the web application with the given root component and config
|
||||||
pub fn launch_cfg(root: fn() -> Element, platform_config: 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);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue