Binary patching rust hot-reloading, sub-second rebuilds, independent server/client hot-reload (#3797)

Enable runtime rust hot patching.

* roll back wry to 0.45

* cleanup rustc

* wip

* wip: fix client/server

* get the subsecond cli thing working again

* back to list of builds but with accessors

* migrate some methods to workspace, clean up impl a bit

* pass mode through build context

* use a build context, similar to cargo/rustc

* merge BuildUpdate and HandleUpdate into BuilderUpdate

* Move more resolution of args into runner

* migrate some methods to runner

* hoist out fullstack

* fix build request resolver

* yay request is cleaned up

* fixup resolution a bit

* spawn off the build!

* re-wire android autodetection

* re-wire android autodetection

* wire back up a few more things

* re-wire tooling verification

* I think it's mostly in the right condition for regular app

* add depinfo parser

* yay okay works with regular apps again

* full rebuilds are back

* rewire file wtcher

* wire patch!

* yayyyyyy patching works and is really fast

* yay android works

* clean up stdout piping

* yayyy memap on android so we don't need root

* create a static wasm jump table

* wip: global offset table for wasm

* wip... thinking about making the patches relocatable

* wip: customized relocation

* YESSSSSS RELOCATABLE WASM

* clean up impl a bit

* lil bit more cleanup

* lil bit more

* lil bit more cleanup

* lil bit more cleanup, support 32 bit platforms

* sick, wasm is completely pic

* hmmmm not quite working yet

* woooo, patches loading from rust+wasm

* integrated into the cli 😎

* condense a bit

* cleaned up request a bit

* bust fingerprints

* Make the file diffing logic a bit more sensible

* still working through a few issues, server launches but is acting weird

* fix merge conflict

* more merge issues

* remove stuff we don't want anymore

* revert name change

* wip: server not working anymore :(

* split apart "dioxus fullstack" into "dioxus server" and "dioxus web"

* fixup a few more compile errors

* grrr opening server

* simultaneous frontend and backend patching

* use rustc wrapper

* wip: passing assets to jumptable

* migrate project-like examples

* patchy  patchy server fns!

* rollback some random changes

* unwind the js files

* rollback hash

* no need for patch

* more cleanups

* lil bit more cleanup, remove some old cruft

* allow patching when wasm webpage is closed

* tiny bit more robust

* lil bit of clean up

* clean up ws code

* bit more clean ups

* condense

* undo file moves

* move back other project

* move other project

* migrate out harness and janky CLI

* fix compile

* anonymize some files, heavily document others

* lots more documentation, for posterity!

* ton more docs

* clean up the cli a bit more

* more ws cleanup

* more cleanup

* migrate build id to cli-config

* add command file handling

* random small cleanups

* wip....

* wip....

* fix: use workspace for krate discovery

* fix panic logging on serve by placing it *after* the logs

* reorder logging to be sensible on success

* bring back swc

* ws cruft

* small patches to relocs in loader

* bump krates

* hoist main funcs into the ifunc table, fixing wasm

* fix tui output for long lines

* add more linker logging

* wow, incredible, we build fat binaries, holy heckkkkk

* fix args for frontend/backend

* small cleanups

* properly send along build IDs to clients

* fix workspace compiles

* clean up jump table work a bit

* clean up logging a bit

* open existing

* fixup some commands

* open existing browser integration

* wire up the new dx serve client/server syntax

* fixup `dx run` command

* bring back some old functionality

* fix serverfn feature sets

* remove toast animation and simplify its code a bit

* less intrusive toast

* dont change build status when patching on web

* add proper cache busting to vcomponent

* clean up the patch file a bit

* more lints/checks cleaned up

* go back to TargetArgs but a BuildTargets resolved struct

* clean up more nits

* use an atomicptr for jumptable

* fix interaction with initial web load + suspense

* don't run asset system on wasm/bindgen js in dev

* reduce blast radius of PR

* use profile to determine if we're in release

* cleanup prod html template

* cleanup profiles

* fix feature resolution of fullstack

* if fullstack is explicitly false, don't use it

* light cleanups

* drop carg config2

* pass along linker args properly

* make workspace examples compile with dx again

* fewer unwraps and better error loggign

* bit more error handlign

* small cleanup

* drive-by cleanups

* use queries instead of initialize for aslr stuff

* fix hotpatch with higher opt level

* fix aslr bug from multiple clients

* fix merge conflict

* fix typos

* clippy

* fix miscompile

* fix doctest

* properly rollback wry to 0.45

* fix markdown path issues, other test issues

* fix test in router

* fix release mode test

* fix some more tests, clean up a few more items

* use fnptr instead of typeid

* clean up memozation via subsecond in core

* use "main" as sentinel

* fix imports and re-exports

* get off __rust_alloc for aslr offset detection

* wip

* wip... fixing unoptimized web

* hmmmm mmmm

* close,ish, still missing the wasmbindgen dynaimc imports

* aha! full wasm works now

- disable externref when compilng patch modules
- manually link wasm instrinsics
- displace the __wbindgen_placeholder__ table with the synthesized wbg imports

what an awful pain.

There might be a few items missing, so will test against the docsite shortly

* remove failing test

* hmmmm things aren't perfect

* from what I can tell, relocatable wasm just doesn't work with wasm-bindgen

sad

https://github.com/rustwasm/wasm-bindgen/issues/1420

* AI IS LITERALLY THE WORST

* preserve wasm snippets across compiles

* IT HOTPATCHES THE DOCSITE, WE ARE LIVING IN THE FUTURE

* remove the macos cfg-out

* fix: need to allocate 1 more page

* fix url issue

* properly span props

* properly fill in extra symbols

* light cleanups and good docs

* delete cruft, better docs

* More cleanups and simplifications

* clippy, typos

* remove harnesses

* wasm-bindgen error logging

* clean up deps

* remove cruft

* proper priv/pub

* use ifuncs for all env imports

* cruft

* use par_iter, better output

* implement a cache for the base module

* delete custom sections

* cache the main native module too

* add par iter in a few more places

* clippy, map ident

* better logging and status bars

* wip: some small windows support

* wip more windowss

* fix weird regression

* implement windows jump table

* small fixes for windows

* better windows support

* windows support pt2

* rust lld is not msvc

* whole archive the rlibs

* ignore windows and sys crates

* logging

* ohhhhhh, pop out dlls

* dedupe

* thANK GOD FAT LINKING WORKS ON WINDOWS

* hoist caching

* implement patch based on cache

* fix location for pdb

* pass data symbols, correct object

* add all symbol for windows

* add log

* treat none rva as undefined

* whoops

* handle windows calling convention

* dont use relocated arm64 stub windows

* not all are text?

* WINDOWS FUNCTION PATCHING WORKS

but issue with statics?

* use our own entry

* handle windows imp symbols

* add log

* jump to the real symbol not the layout

* whoops

* hotpatching windows works completely holy heck yay

* drop linker args crate

* disable ssg playwright for now

* light cleanups

* some cleanups / preps:
- bump to serverfn 0.8.2
- fix playwright test
- fix clippy

* use fix from #4042

* bring in other fix for liveview

* hoist some configuration

* use __aslr_reference instead of aslr_reference

* add better errors to patcherror

* fix android

* panic if patching fails

* clean up tomls

* wip: fixing wasm compile for server

* fullstack wasm again

* fix cfg

* fixup feature detection

* fix issue with features

* fix playwright!

* add barebones template test harness

* fix compatibility between the two hotreload engines

* fix preldue of server

* fix check for wasm-opt

* small fixes
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2025-05-06 23:08:59 -07:00
1 parent 71d85e6dd5
commit d976ca184a
142 files changed
+17982 -7156

No files matched your search

+14 -41
View File
@@ -12,7 +12,7 @@ resolver = "2"
[dependencies]
# server functions
server_fn = { version = "0.8.0-rc1", features = ["browser"], default-features = false }
server_fn = { workspace = true, default-features = false }
dioxus_server_macro = { workspace = true }
# axum
@@ -23,13 +23,11 @@ dioxus-lib = { workspace = true }
generational-box = { workspace = true }
# Dioxus + SSR
dioxus-ssr = { workspace = true, optional = true }
dioxus-server = { workspace = true, optional = true}
dioxus-isrg = { workspace = true, optional = true }
dioxus-router = { workspace = true, features = ["streaming"], optional = true }
dioxus-fullstack-hooks = { workspace = true }
dioxus-fullstack-protocol = { workspace = true }
hyper = { workspace = true, optional = true }
http = { workspace = true, optional = true }
# Web Integration
dioxus-web = { workspace = true, features = ["hydrate"], default-features = false, optional = true }
@@ -77,12 +75,6 @@ dioxus-devtools = { workspace = true, optional = true }
aws-lc-rs = { version = "1.12.5", optional = true }
dioxus-history = { workspace = true }
[target.'cfg(target_arch = "wasm32")'.dependencies]
tokio = { workspace = true, features = ["rt", "sync"], optional = true }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio = { workspace = true, features = ["rt", "sync", "rt-multi-thread"], optional = true }
[dev-dependencies]
dioxus = { workspace = true, features = ["fullstack"] }
@@ -92,43 +84,24 @@ devtools = ["dioxus-web?/devtools", "dep:dioxus-devtools"]
mounted = ["dioxus-web?/mounted"]
file_engine = ["dioxus-web?/file_engine"]
document = ["dioxus-web?/document"]
web = ["dep:dioxus-web", "dep:web-sys", "dioxus-fullstack-hooks/web"]
desktop = ["dep:dioxus-desktop", "server_fn/reqwest", "dioxus_server_macro/reqwest"]
mobile = ["dep:dioxus-mobile", "server_fn/reqwest", "dioxus_server_macro/reqwest"]
web = ["dep:dioxus-web", "dep:web-sys", "dioxus-fullstack-hooks/web", "server_fn/browser"]
desktop = ["server_fn/reqwest", "dioxus_server_macro/reqwest"]
mobile = ["server_fn/reqwest", "dioxus_server_macro/reqwest"]
default-tls = ["server_fn/default-tls"]
rustls = ["server_fn/rustls", "dep:rustls", "dep:hyper-rustls"]
axum_core = [
"dep:axum",
server = [
"server-core" ,
"default-tls",
"server_fn/axum",
]
server-core = [
"server_fn/axum-no-default",
"dioxus_server_macro/axum",
"default-tls",
"server",
]
axum = [
"dep:tower-http",
"server_fn/axum",
"axum_core",
]
server = [
"server_fn/reqwest",
"server_fn/ssr",
"dioxus_server_macro/server",
"dioxus_server_macro/reqwest",
"dioxus-fullstack-hooks/server",
"dep:tokio",
"dep:tokio-util",
"dep:tokio-stream",
"dep:dioxus-ssr",
"dep:dioxus-isrg",
"dep:dioxus-router",
"dep:tower",
"dep:hyper",
"dep:http",
"dep:tower-layer",
"dep:tracing-futures",
"dep:pin-project",
"dep:thiserror",
"dep:dioxus-cli-config",
"dep:async-trait",
"dep:parking_lot",
"dep:dioxus-server",
"dioxus-interpreter-js",
]
aws-lc-rs = ["dep:aws-lc-rs"]
+1 -1
View File
@@ -72,7 +72,7 @@ First, make sure your `axum` dependency is optional and enabled by the server fe
```toml
[dependencies]
dioxus = { version = "*", features = ["fullstack"] }
axum = { version = "0.7.0", optional = true }
axum = { version = "0.8.0", optional = true }
tokio = { version = "1.0", features = ["full"], optional = true }
dioxus-cli-config = { version = "*", optional = true }
-379
View File
@@ -1,379 +0,0 @@
//! Dioxus core utilities for the [Axum](https://docs.rs/axum/latest/axum/index.html) server framework.
//!
//! # Example
//! ```rust, no_run
//! #![allow(non_snake_case)]
//! use dioxus::prelude::*;
//!
//! fn main() {
//! #[cfg(feature = "web")]
//! // Hydrate the application on the client
//! dioxus::launch(app);
//! #[cfg(feature = "server")]
//! {
//! tokio::runtime::Runtime::new()
//! .unwrap()
//! .block_on(async move {
//! // Get the address the server should run on. If the CLI is running, the CLI proxies fullstack into the main address
//! // and we use the generated address the CLI gives us
//! let address = dioxus::cli_config::fullstack_address_or_localhost();
//! let listener = tokio::net::TcpListener::bind(address)
//! .await
//! .unwrap();
//! axum::serve(
//! listener,
//! axum::Router::new()
//! // Server side render the application, serve static assets, and register server functions
//! .register_server_functions()
//! .fallback(get(render_handler)
//! // Note: ServeConfig::new won't work on WASM
//! .with_state(RenderHandler::new(ServeConfig::new().unwrap(), app))
//! )
//! .into_make_service(),
//! )
//! .await
//! .unwrap();
//! });
//! }
//! }
//!
//! fn app() -> Element {
//! let mut text = use_signal(|| "...".to_string());
//!
//! rsx! {
//! button {
//! onclick: move |_| async move {
//! if let Ok(data) = get_server_data().await {
//! text.set(data);
//! }
//! },
//! "Run a server function"
//! }
//! "Server said: {text}"
//! }
//! }
//!
//! #[server(GetServerData)]
//! async fn get_server_data() -> Result<String, ServerFnError> {
//! Ok("Hello from the server!".to_string())
//! }
//!
//! # WASM support
//!
//! These utilities compile to the WASM family of targets, while the more complete ones found in [server] don't
//! ```
use std::sync::Arc;
use crate::prelude::*;
use crate::render::SSRError;
use crate::ContextProviders;
use axum::body;
use axum::extract::State;
use axum::routing::*;
use axum::{
body::Body,
http::{Request, Response, StatusCode},
response::IntoResponse,
};
use dioxus_lib::prelude::{Element, VirtualDom};
use http::header::*;
/// A extension trait with server function utilities for integrating Dioxus with your Axum router.
pub trait DioxusRouterFnExt<S> {
/// Registers server functions with the default handler. This handler function will pass an empty [`DioxusServerContext`] to your server functions.
///
/// # Example
/// ```rust, no_run
/// # use dioxus_lib::prelude::*;
/// # use dioxus_fullstack::prelude::*;
/// #[tokio::main]
/// async fn main() {
/// let addr = dioxus::cli_config::fullstack_address_or_localhost();
/// let router = axum::Router::new()
/// // Register server functions routes with the default handler
/// .register_server_functions()
/// .into_make_service();
/// let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
/// axum::serve(listener, router).await.unwrap();
/// }
/// ```
#[allow(dead_code)]
fn register_server_functions(self) -> Self
where
Self: Sized,
{
self.register_server_functions_with_context(Default::default())
}
/// Registers server functions with some additional context to insert into the [`DioxusServerContext`] for that handler.
///
/// # Example
/// ```rust, no_run
/// # use dioxus_lib::prelude::*;
/// # use dioxus_fullstack::prelude::*;
/// # use std::sync::Arc;
/// #[tokio::main]
/// async fn main() {
/// let addr = dioxus::cli_config::fullstack_address_or_localhost();
/// let router = axum::Router::new()
/// // Register server functions routes with the default handler
/// .register_server_functions_with_context(Arc::new(vec![Box::new(|| Box::new(1234567890u32))]))
/// .into_make_service();
/// let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
/// axum::serve(listener, router).await.unwrap();
/// }
/// ```
fn register_server_functions_with_context(self, context_providers: ContextProviders) -> Self;
}
impl<S> DioxusRouterFnExt<S> for Router<S>
where
S: Send + Sync + Clone + 'static,
{
fn register_server_functions_with_context(
mut self,
context_providers: ContextProviders,
) -> Self {
use http::method::Method;
for (path, method) in server_fn::axum::server_fn_paths() {
tracing::trace!("Registering server function: {} {}", method, path);
let context_providers = context_providers.clone();
let handler = move |req| handle_server_fns_inner(path, context_providers, req);
self = match method {
Method::GET => self.route(path, get(handler)),
Method::POST => self.route(path, post(handler)),
Method::PUT => self.route(path, put(handler)),
_ => unimplemented!("Unsupported server function method: {}", method),
};
}
self
}
}
/// A handler for Dioxus server functions. This will run the server function and return the result.
async fn handle_server_fns_inner(
path: &str,
additional_context: ContextProviders,
req: Request<Body>,
) -> impl IntoResponse {
let path_string = path.to_string();
let (parts, body) = req.into_parts();
let req = Request::from_parts(parts.clone(), body);
let method = req.method().clone();
if let Some(mut service) =
server_fn::axum::get_server_fn_service(&path_string, method)
{
// Create the server context with info from the request
let server_context = DioxusServerContext::new(parts);
// Provide additional context from the render state
add_server_context(&server_context, &additional_context);
// store Accepts and Referrer in case we need them for redirect (below)
let accepts_html = req
.headers()
.get(ACCEPT)
.and_then(|v| v.to_str().ok())
.map(|v| v.contains("text/html"))
.unwrap_or(false);
let referrer = req.headers().get(REFERER).cloned();
// actually run the server fn (which may use the server context)
let fut = with_server_context(server_context.clone(), || service.run(req));
let mut res = ProvideServerContext::new(fut, server_context.clone()).await;
// it it accepts text/html (i.e., is a plain form post) and doesn't already have a
// Location set, then redirect to Referer
if accepts_html {
if let Some(referrer) = referrer {
let has_location = res.headers().get(LOCATION).is_some();
if !has_location {
*res.status_mut() = StatusCode::FOUND;
res.headers_mut().insert(LOCATION, referrer);
}
}
}
// apply the response parts from the server context to the response
server_context.send_response(&mut res);
Ok(res)
} else {
Response::builder().status(StatusCode::BAD_REQUEST).body(
{
#[cfg(target_family = "wasm")]
{
Body::from(format!(
"No server function found for path: {path_string}\nYou may need to explicitly register the server function with `register_explicit`, rebuild your wasm binary to update a server function link or make sure the prefix your server and client use for server functions match.",
))
}
#[cfg(not(target_family = "wasm"))]
{
Body::from(format!(
"No server function found for path: {path_string}\nYou may need to rebuild your wasm binary to update a server function link or make sure the prefix your server and client use for server functions match.",
))
}
}
)
}
.expect("could not build Response")
}
pub(crate) fn add_server_context(
server_context: &DioxusServerContext,
context_providers: &ContextProviders,
) {
for index in 0..context_providers.len() {
let context_providers = context_providers.clone();
server_context.insert_boxed_factory(Box::new(move || context_providers[index]()));
}
}
/// State used by [`render_handler`] to render a dioxus component with axum
#[derive(Clone)]
pub struct RenderHandleState {
config: ServeConfig,
build_virtual_dom: Arc<dyn Fn() -> VirtualDom + Send + Sync>,
ssr_state: once_cell::sync::OnceCell<SSRState>,
}
impl RenderHandleState {
/// Create a new [`RenderHandleState`]
pub fn new(config: ServeConfig, root: fn() -> Element) -> Self {
Self {
config,
build_virtual_dom: Arc::new(move || VirtualDom::new(root)),
ssr_state: Default::default(),
}
}
/// Create a new [`RenderHandleState`] with a custom [`VirtualDom`] factory. This method can be used to pass context into the root component of your application.
pub fn new_with_virtual_dom_factory(
config: ServeConfig,
build_virtual_dom: impl Fn() -> VirtualDom + Send + Sync + 'static,
) -> Self {
Self {
config,
build_virtual_dom: Arc::new(build_virtual_dom),
ssr_state: Default::default(),
}
}
/// Set the [`ServeConfig`] for this [`RenderHandleState`]
pub fn with_config(mut self, config: ServeConfig) -> Self {
self.config = config;
self
}
/// Set the [`SSRState`] for this [`RenderHandleState`]. Sharing a [`SSRState`] between multiple [`RenderHandleState`]s is more efficient than creating a new [`SSRState`] for each [`RenderHandleState`].
pub fn with_ssr_state(mut self, ssr_state: SSRState) -> Self {
self.ssr_state = once_cell::sync::OnceCell::new();
if self.ssr_state.set(ssr_state).is_err() {
panic!("SSRState already set");
}
self
}
fn ssr_state(&self) -> &SSRState {
self.ssr_state.get_or_init(|| SSRState::new(&self.config))
}
}
/// SSR renderer handler for Axum with added context injection.
///
/// # Example
/// ```rust,no_run
/// #![allow(non_snake_case)]
/// use std::sync::{Arc, Mutex};
///
/// use axum::routing::get;
/// use dioxus::prelude::*;
///
/// fn app() -> Element {
/// rsx! {
/// "hello!"
/// }
/// }
///
/// #[tokio::main]
/// async fn main() {
/// let addr = dioxus::cli_config::fullstack_address_or_localhost();
/// let router = axum::Router::new()
/// // Register server functions, etc.
/// // Note you can use `register_server_functions_with_context`
/// // to inject the context into server functions running outside
/// // of an SSR render context.
/// .fallback(get(render_handler)
/// .with_state(RenderHandleState::new(ServeConfig::new().unwrap(), app))
/// )
/// .into_make_service();
/// let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
/// axum::serve(listener, router).await.unwrap();
/// }
/// ```
pub async fn render_handler(
State(state): State<RenderHandleState>,
request: Request<Body>,
) -> impl IntoResponse {
let cfg = &state.config;
let ssr_state = state.ssr_state();
let build_virtual_dom = {
let build_virtual_dom = state.build_virtual_dom.clone();
let context_providers = state.config.context_providers.clone();
move || {
let mut vdom = build_virtual_dom();
for state in context_providers.as_slice() {
vdom.insert_any_root_context(state());
}
vdom
}
};
let (parts, _) = request.into_parts();
let url = parts
.uri
.path_and_query()
.ok_or(StatusCode::BAD_REQUEST)?
.to_string();
let parts: Arc<parking_lot::RwLock<http::request::Parts>> =
Arc::new(parking_lot::RwLock::new(parts));
// Create the server context with info from the request
let server_context = DioxusServerContext::from_shared_parts(parts.clone());
// Provide additional context from the render state
add_server_context(&server_context, &state.config.context_providers);
match ssr_state
.render(url, cfg, build_virtual_dom, &server_context)
.await
{
Ok((freshness, rx)) => {
let mut response = axum::response::Html::from(Body::from_stream(rx)).into_response();
freshness.write(response.headers_mut());
server_context.send_response(&mut response);
Result::<http::Response<axum::body::Body>, StatusCode>::Ok(response)
}
Err(SSRError::Incremental(e)) => {
tracing::error!("Failed to render page: {}", e);
Ok(report_err(e).into_response())
}
Err(SSRError::Routing(e)) => {
tracing::trace!("Page not found: {}", e);
Ok(Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Body::from("Page not found"))
.unwrap())
}
}
}
fn report_err<E: std::fmt::Display>(e: E) -> Response<axum::body::Body> {
Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(body::Body::new(format!("Error: {}", e)))
.unwrap()
}
-14
View File
@@ -1,14 +0,0 @@
//! This module contains the document providers for the fullstack platform.
#[cfg(feature = "server")]
pub mod server;
use dioxus_fullstack_protocol::SerializeContextEntry;
#[cfg(feature = "server")]
pub use server::ServerDocument;
#[cfg(all(feature = "web", feature = "document"))]
pub mod web;
#[allow(unused)]
pub(crate) fn head_element_hydration_entry() -> SerializeContextEntry<bool> {
dioxus_fullstack_protocol::serialize_context().create_entry()
}
-155
View File
@@ -1,155 +0,0 @@
//! On the server, we collect any elements that should be rendered into the head in the first frame of SSR.
//! After the first frame, we have already sent down the head, so we can't modify it in place. The web client
//! will hydrate the head with the correct contents once it loads.
use std::cell::RefCell;
use dioxus_lib::{document::*, prelude::*};
use dioxus_ssr::Renderer;
use once_cell::sync::Lazy;
use parking_lot::RwLock;
static RENDERER: Lazy<RwLock<Renderer>> = Lazy::new(|| RwLock::new(Renderer::new()));
#[derive(Default)]
struct ServerDocumentInner {
streaming: bool,
title: Option<String>,
meta: Vec<Element>,
link: Vec<Element>,
script: Vec<Element>,
}
/// A Document provider that collects all contents injected into the head for SSR rendering.
#[derive(Default)]
pub struct ServerDocument(RefCell<ServerDocumentInner>);
impl ServerDocument {
pub(crate) fn title(&self) -> Option<String> {
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! {
{myself.meta.iter().map(|m| rsx! { {m} })}
{myself.link.iter().map(|l| rsx! { {l} })}
{myself.script.iter().map(|s| rsx! { {s} })}
};
RENDERER.write().render_element_to(to, element)?;
Ok(())
}
pub(crate) fn start_streaming(&self) {
self.0.borrow_mut().streaming = true;
}
pub(crate) fn warn_if_streaming(&self) {
if self.0.borrow().streaming {
tracing::warn!("Attempted to insert content into the head after the initial streaming frame. Inserting content into the head only works during the initial render of SSR outside before resolving any suspense boundaries.");
}
}
/// 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
#[track_caller]
pub(crate) fn serialize_for_hydration(&self) {
// We only serialize the head elements if the web document feature is enabled
#[cfg(feature = "document")]
{
super::head_element_hydration_entry()
.insert(&!self.0.borrow().streaming, std::panic::Location::caller());
}
}
}
impl Document for ServerDocument {
fn eval(&self, js: String) -> Eval {
NoOpDocument.eval(js)
}
fn set_title(&self, title: String) {
self.warn_if_streaming();
self.0.borrow_mut().title = Some(title);
}
fn create_meta(&self, props: MetaProps) {
self.0.borrow_mut().meta.push(rsx! {
meta {
name: props.name,
charset: props.charset,
http_equiv: props.http_equiv,
content: props.content,
property: props.property,
..props.additional_attributes,
}
});
}
fn create_script(&self, props: ScriptProps) {
let children = props.script_contents().ok();
self.0.borrow_mut().script.push(rsx! {
script {
src: props.src,
defer: props.defer,
crossorigin: props.crossorigin,
fetchpriority: props.fetchpriority,
integrity: props.integrity,
nomodule: props.nomodule,
nonce: props.nonce,
referrerpolicy: props.referrerpolicy,
r#type: props.r#type,
..props.additional_attributes,
{children}
}
});
}
fn create_style(&self, props: StyleProps) {
let contents = props.style_contents().ok();
self.0.borrow_mut().script.push(rsx! {
style {
media: props.media,
nonce: props.nonce,
title: props.title,
..props.additional_attributes,
{contents}
}
})
}
fn create_link(&self, props: LinkProps) {
self.0.borrow_mut().link.push(rsx! {
link {
rel: props.rel,
media: props.media,
title: props.title,
disabled: props.disabled,
r#as: props.r#as,
sizes: props.sizes,
href: props.href,
crossorigin: props.crossorigin,
referrerpolicy: props.referrerpolicy,
fetchpriority: props.fetchpriority,
hreflang: props.hreflang,
integrity: props.integrity,
r#type: props.r#type,
blocking: props.blocking,
..props.additional_attributes,
}
})
}
fn create_head_component(&self) -> bool {
self.warn_if_streaming();
self.serialize_for_hydration();
true
}
}
+8 -49
View File
@@ -6,63 +6,22 @@
pub use once_cell;
#[cfg(feature = "axum")]
#[cfg_attr(docsrs, doc(cfg(feature = "axum")))]
pub mod server;
#[cfg(all(feature = "web", feature = "document"))]
mod web;
#[cfg(feature = "axum_core")]
#[cfg_attr(docsrs, doc(cfg(feature = "axum_core")))]
pub mod axum_core;
pub mod document;
#[cfg(feature = "server")]
mod render;
#[cfg(feature = "server")]
mod streaming;
#[cfg(all(feature = "web", feature = "document"))]
pub use web::FullstackWebDocument;
#[cfg(feature = "server")]
mod serve_config;
#[cfg(feature = "server")]
pub use serve_config::*;
#[cfg(feature = "server")]
mod server_context;
pub use dioxus_server::*;
/// A prelude of commonly used items in dioxus-fullstack.
pub mod prelude {
pub use dioxus_fullstack_hooks::*;
#[cfg(feature = "axum")]
#[cfg_attr(docsrs, doc(cfg(feature = "axum")))]
pub use crate::server::*;
#[cfg(feature = "axum_core")]
pub use crate::axum_core::*;
#[cfg(feature = "server")]
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
pub use crate::render::{FullstackHTMLTemplate, SSRState};
#[cfg(feature = "server")]
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
pub use crate::serve_config::{ServeConfig, ServeConfigBuilder};
#[cfg(feature = "axum")]
#[cfg_attr(docsrs, doc(cfg(feature = "axum")))]
pub use crate::server_context::Axum;
#[cfg(feature = "server")]
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
pub use crate::server_context::{
extract, server_context, with_server_context, DioxusServerContext, FromContext,
FromServerContext, ProvideServerContext,
};
#[cfg(feature = "server")]
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
pub use dioxus_isrg::{IncrementalRenderer, IncrementalRendererConfig};
pub use dioxus_server_macro::*;
pub use server_fn::{self, ServerFn as _, ServerFnError};
#[cfg(feature = "server")]
pub use dioxus_server::prelude::*;
}
-736
View File
@@ -1,736 +0,0 @@
//! A shared pool of renderers for efficient server side rendering.
use crate::document::ServerDocument;
use crate::streaming::{Mount, StreamingRenderer};
use dioxus_cli_config::base_path;
use dioxus_fullstack_hooks::{StreamingContext, StreamingStatus};
use dioxus_fullstack_protocol::{HydrationContext, SerializedHydrationData};
use dioxus_interpreter_js::INITIALIZE_STREAMING_JS;
use dioxus_isrg::{CachedRender, IncrementalRendererError, RenderFreshness};
use dioxus_lib::document::Document;
use dioxus_lib::prelude::dioxus_core::DynamicNode;
use dioxus_router::prelude::ParseRouteError;
use dioxus_ssr::Renderer;
use futures_channel::mpsc::Sender;
use futures_util::{Stream, StreamExt};
use std::fmt::Write;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::RwLock;
use std::{collections::HashMap, future::Future};
use tokio::task::JoinHandle;
use crate::{prelude::*, StreamingMode};
use dioxus_lib::prelude::*;
/// A suspense boundary that is pending with a placeholder in the client
struct PendingSuspenseBoundary {
mount: Mount,
children: Vec<ScopeId>,
}
/// Spawn a task in the background. If wasm is enabled, this will use the single threaded tokio runtime
fn spawn_platform<Fut>(f: impl FnOnce() -> Fut + Send + 'static) -> JoinHandle<Fut::Output>
where
Fut: Future + 'static,
Fut::Output: Send + 'static,
{
#[cfg(not(target_arch = "wasm32"))]
{
use tokio_util::task::LocalPoolHandle;
static TASK_POOL: std::sync::OnceLock<LocalPoolHandle> = std::sync::OnceLock::new();
let pool = TASK_POOL.get_or_init(|| {
let threads = std::thread::available_parallelism()
.unwrap_or(std::num::NonZeroUsize::new(1).unwrap());
LocalPoolHandle::new(threads.into())
});
pool.spawn_pinned(f)
}
#[cfg(target_arch = "wasm32")]
{
tokio::task::spawn_local(f())
}
}
fn in_root_scope<T>(virtual_dom: &VirtualDom, f: impl FnOnce() -> T) -> T {
virtual_dom.in_runtime(|| ScopeId::ROOT.in_runtime(f))
}
/// Errors that can occur during server side rendering before the initial chunk is sent down
pub enum SSRError {
/// An error from the incremental renderer. This should result in a 500 code
Incremental(IncrementalRendererError),
/// An error from the dioxus router. This should result in a 404 code
Routing(ParseRouteError),
}
struct SsrRendererPool {
renderers: RwLock<Vec<Renderer>>,
incremental_cache: Option<RwLock<dioxus_isrg::IncrementalRenderer>>,
}
impl SsrRendererPool {
fn new(
initial_size: usize,
incremental: Option<dioxus_isrg::IncrementalRendererConfig>,
) -> Self {
let renderers = RwLock::new((0..initial_size).map(|_| pre_renderer()).collect());
Self {
renderers,
incremental_cache: incremental.map(|cache| RwLock::new(cache.build())),
}
}
/// Look for a cached route in the incremental cache and send it into the render channel if it exists
fn check_cached_route(
&self,
route: &str,
render_into: &mut Sender<Result<String, dioxus_isrg::IncrementalRendererError>>,
) -> Option<RenderFreshness> {
if let Some(incremental) = &self.incremental_cache {
if let Ok(mut incremental) = incremental.write() {
match incremental.get(route) {
Ok(Some(cached_render)) => {
let CachedRender {
freshness,
response,
..
} = cached_render;
_ = render_into.start_send(String::from_utf8(response.to_vec()).map_err(
|err| dioxus_isrg::IncrementalRendererError::Other(Box::new(err)),
));
return Some(freshness);
}
Err(e) => {
tracing::error!(
"Failed to get route \"{route}\" from incremental cache: {e}"
);
}
_ => {}
}
}
}
None
}
/// Render a virtual dom into a stream. This method will return immediately and continue streaming the result in the background
/// The streaming is canceled when the stream the function returns is dropped
async fn render_to(
self: Arc<Self>,
cfg: &ServeConfig,
route: String,
virtual_dom_factory: impl FnOnce() -> VirtualDom + Send + Sync + 'static,
server_context: &DioxusServerContext,
) -> Result<
(
RenderFreshness,
impl Stream<Item = Result<String, dioxus_isrg::IncrementalRendererError>>,
),
SSRError,
> {
struct ReceiverWithDrop {
receiver: futures_channel::mpsc::Receiver<
Result<String, dioxus_isrg::IncrementalRendererError>,
>,
cancel_task: Option<tokio::task::JoinHandle<()>>,
}
impl Stream for ReceiverWithDrop {
type Item = Result<String, dioxus_isrg::IncrementalRendererError>;
fn poll_next(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
self.receiver.poll_next_unpin(cx)
}
}
// When we drop the stream, we need to cancel the task that is feeding values to the stream
impl Drop for ReceiverWithDrop {
fn drop(&mut self) {
if let Some(cancel_task) = self.cancel_task.take() {
cancel_task.abort();
}
}
}
let (mut into, rx) = futures_channel::mpsc::channel::<
Result<String, dioxus_isrg::IncrementalRendererError>,
>(1000);
let (initial_result_tx, initial_result_rx) = futures_channel::oneshot::channel();
// before we even spawn anything, we can check synchronously if we have the route cached
if let Some(freshness) = self.check_cached_route(&route, &mut into) {
return Ok((
freshness,
ReceiverWithDrop {
receiver: rx,
cancel_task: None,
},
));
}
let wrapper = FullstackHTMLTemplate { cfg: cfg.clone() };
let server_context = server_context.clone();
let mut renderer = self
.renderers
.write()
.unwrap()
.pop()
.unwrap_or_else(pre_renderer);
let myself = self.clone();
let streaming_mode = cfg.streaming_mode;
let create_render_future = move || async move {
let mut virtual_dom = virtual_dom_factory();
let document = std::rc::Rc::new(crate::document::server::ServerDocument::default());
virtual_dom.provide_root_context(document.clone());
// If there is a base path, trim the base path from the route and add the base path formatting to the
// history provider
let history;
if let Some(base_path) = base_path() {
let base_path = base_path.trim_matches('/');
let base_path = format!("/{base_path}");
let route = route.strip_prefix(&base_path).unwrap_or(&route);
history =
dioxus_history::MemoryHistory::with_initial_path(route).with_prefix(base_path);
} else {
history = dioxus_history::MemoryHistory::with_initial_path(&route);
}
let streaming_context = in_root_scope(&virtual_dom, StreamingContext::new);
virtual_dom.provide_root_context(Rc::new(history) as Rc<dyn dioxus_history::History>);
virtual_dom.provide_root_context(document.clone() as std::rc::Rc<dyn Document>);
virtual_dom.provide_root_context(streaming_context);
// rebuild the virtual dom
virtual_dom.rebuild_in_place();
// If streaming is disabled, wait for the virtual dom to finish all suspense work
// before rendering anything
if streaming_mode == StreamingMode::Disabled {
virtual_dom.wait_for_suspense().await;
}
// Otherwise, just wait for the streaming context to signal the initial chunk is ready
else {
loop {
// Check if the router has finished and set the streaming context to finished
let streaming_context_finished =
in_root_scope(&virtual_dom, || streaming_context.current_status())
== StreamingStatus::InitialChunkCommitted;
// Or if this app isn't using the router and has finished suspense
let suspense_finished = !virtual_dom.suspended_tasks_remaining();
if streaming_context_finished || suspense_finished {
break;
}
// Wait for new async work that runs during suspense (mainly use_server_futures)
virtual_dom.wait_for_suspense_work().await;
// Do that async work
virtual_dom.render_suspense_immediate().await;
}
}
// check if there are any errors
let errors = virtual_dom.in_runtime(|| {
let error_context: ErrorContext = ScopeId::APP
.consume_context()
.expect("The root should be under an error boundary");
let errors = error_context.errors();
errors.to_vec()
});
if errors.is_empty() {
// If routing was successful, we can return a 200 status and render into the stream
_ = initial_result_tx.send(Ok(()));
} else {
// If there was an error while routing, return the error with a 400 status
// Return a routing error if any of the errors were a routing error
let routing_error = errors.iter().find_map(|err| err.downcast().cloned());
if let Some(routing_error) = routing_error {
_ = initial_result_tx.send(Err(SSRError::Routing(routing_error)));
return;
}
#[derive(thiserror::Error, Debug)]
#[error("{0}")]
pub struct ErrorWhileRendering(String);
let mut all_errors = String::new();
for error in errors {
all_errors += &error.to_string();
all_errors += "\n"
}
let error = ErrorWhileRendering(all_errors);
_ = initial_result_tx.send(Err(SSRError::Incremental(
IncrementalRendererError::Other(Box::new(error)),
)));
return;
}
let mut pre_body = String::new();
if let Err(err) = wrapper.render_head(&mut pre_body, &virtual_dom) {
_ = into.start_send(Err(err));
return;
}
let stream = Arc::new(StreamingRenderer::new(pre_body, into));
let scope_to_mount_mapping = Arc::new(RwLock::new(HashMap::new()));
renderer.pre_render = true;
{
let scope_to_mount_mapping = scope_to_mount_mapping.clone();
let stream = stream.clone();
renderer.set_render_components(streaming_render_component_callback(
stream,
scope_to_mount_mapping,
));
}
macro_rules! throw_error {
($e:expr) => {
stream.close_with_error($e);
return;
};
}
// Render the initial frame with loading placeholders
let mut initial_frame = renderer.render(&virtual_dom);
// 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, &virtual_dom) {
throw_error!(err);
}
stream.render(initial_frame);
// After the initial render, we need to resolve suspense
while virtual_dom.suspended_tasks_remaining() {
virtual_dom.wait_for_suspense_work().await;
let resolved_suspense_nodes = virtual_dom.render_suspense_immediate().await;
// Just rerender the resolved nodes
for scope in resolved_suspense_nodes {
let pending_suspense_boundary = {
let mut lock = scope_to_mount_mapping.write().unwrap();
lock.remove(&scope)
};
// If the suspense boundary was immediately removed, it may not have a mount. We can just skip resolving it
if let Some(pending_suspense_boundary) = pending_suspense_boundary {
let mut resolved_chunk = String::new();
// After we replace the placeholder in the dom with javascript, we need to send down the resolved data so that the client can hydrate the node
let render_suspense = |into: &mut String| {
renderer.reset_hydration();
renderer.render_scope(into, &virtual_dom, scope)
};
let resolved_data = serialize_server_data(&virtual_dom, scope);
if let Err(err) = stream.replace_placeholder(
pending_suspense_boundary.mount,
render_suspense,
resolved_data,
&mut resolved_chunk,
) {
throw_error!(dioxus_isrg::IncrementalRendererError::RenderError(err));
}
stream.render(resolved_chunk);
// Freeze the suspense boundary to prevent future reruns of any child nodes of the suspense boundary
if let Some(suspense) =
SuspenseContext::downcast_suspense_boundary_from_scope(
&virtual_dom.runtime(),
scope,
)
{
suspense.freeze();
// Go to every child suspense boundary and add an error boundary. Since we cannot rerun any nodes above the child suspense boundary,
// we need to capture the errors and send them to the client as it resolves
virtual_dom.in_runtime(|| {
for &suspense_scope in pending_suspense_boundary.children.iter() {
start_capturing_errors(suspense_scope);
}
});
}
}
}
}
// After suspense is done, we render the html after the body
let mut post_streaming = String::new();
if let Err(err) = wrapper.render_after_body(&mut post_streaming) {
throw_error!(err);
}
// 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_head(&mut cached_render, &virtual_dom) {
throw_error!(err);
}
renderer.reset_hydration();
if let Err(err) = renderer.render_to(&mut cached_render, &virtual_dom) {
throw_error!(dioxus_isrg::IncrementalRendererError::RenderError(err));
}
if let Err(err) = wrapper.render_after_main(&mut cached_render, &virtual_dom) {
throw_error!(err);
}
cached_render.push_str(&post_streaming);
if let Ok(mut incremental) = incremental.write() {
let _ = incremental.cache(route, cached_render);
}
}
stream.render(post_streaming);
renderer.reset_render_components();
myself.renderers.write().unwrap().push(renderer);
};
let join_handle = spawn_platform(move || {
ProvideServerContext::new(create_render_future(), server_context)
});
// Wait for the initial result which determines the status code
initial_result_rx.await.map_err(|err| {
SSRError::Incremental(IncrementalRendererError::Other(Box::new(err)))
})??;
Ok((
RenderFreshness::now(None),
ReceiverWithDrop {
receiver: rx,
cancel_task: Some(join_handle),
},
))
}
}
/// Create the streaming render component callback. It will keep track of what scopes are mounted to what pending
/// suspense boundaries in the DOM.
///
/// This mapping is used to replace the DOM mount with the resolved contents once the suspense boundary is finished.
fn streaming_render_component_callback(
stream: Arc<StreamingRenderer<IncrementalRendererError>>,
scope_to_mount_mapping: Arc<RwLock<HashMap<ScopeId, PendingSuspenseBoundary>>>,
) -> impl Fn(&mut Renderer, &mut dyn Write, &VirtualDom, ScopeId) -> std::fmt::Result
+ Send
+ Sync
+ 'static {
// We use a stack to keep track of what suspense boundaries we are nested in to add children to the correct boundary
// The stack starts with the root scope because the root is a suspense boundary
let pending_suspense_boundaries_stack = RwLock::new(vec![]);
move |renderer, to, vdom, scope| {
let is_suspense_boundary =
SuspenseContext::downcast_suspense_boundary_from_scope(&vdom.runtime(), scope)
.filter(|s| s.has_suspended_tasks())
.is_some();
if is_suspense_boundary {
let mount = stream.render_placeholder(
|to| {
{
pending_suspense_boundaries_stack
.write()
.unwrap()
.push(scope);
}
let out = renderer.render_scope(to, vdom, scope);
{
pending_suspense_boundaries_stack.write().unwrap().pop();
}
out
},
&mut *to,
)?;
// Add the suspense boundary to the list of pending suspense boundaries
// We will replace the mount with the resolved contents later once the suspense boundary is resolved
let mut scope_to_mount_mapping_write = scope_to_mount_mapping.write().unwrap();
scope_to_mount_mapping_write.insert(
scope,
PendingSuspenseBoundary {
mount,
children: vec![],
},
);
// Add the scope to the list of children of the parent suspense boundary
let pending_suspense_boundaries_stack =
pending_suspense_boundaries_stack.read().unwrap();
// If there is a parent suspense boundary, add the scope to the list of children
// This suspense boundary will start capturing errors when the parent is resolved
if let Some(parent) = pending_suspense_boundaries_stack.last() {
let parent = scope_to_mount_mapping_write.get_mut(parent).unwrap();
parent.children.push(scope);
}
// Otherwise this is a root suspense boundary, so we need to start capturing errors immediately
else {
vdom.in_runtime(|| {
start_capturing_errors(scope);
});
}
} else {
renderer.render_scope(to, vdom, scope)?
}
Ok(())
}
}
/// Start capturing errors at a suspense boundary. If the parent suspense boundary is frozen, we need to capture the errors in the suspense boundary
/// and send them to the client to continue bubbling up
fn start_capturing_errors(suspense_scope: ScopeId) {
// Add an error boundary to the scope
suspense_scope.in_runtime(provide_error_boundary);
}
fn serialize_server_data(virtual_dom: &VirtualDom, scope: ScopeId) -> SerializedHydrationData {
// After we replace the placeholder in the dom with javascript, we need to send down the resolved data so that the client can hydrate the node
// Extract any data we serialized for hydration (from server futures)
let html_data = extract_from_suspense_boundary(virtual_dom, scope);
// serialize the server state into a base64 string
html_data.serialized()
}
/// Walks through the suspense boundary in a depth first order and extracts the data from the context API.
/// We use depth first order instead of relying on the order the hooks are called in because during suspense on the server, the order that futures are run in may be non deterministic.
pub(crate) fn extract_from_suspense_boundary(
vdom: &VirtualDom,
scope: ScopeId,
) -> HydrationContext {
let data = HydrationContext::default();
serialize_errors(&data, vdom, scope);
take_from_scope(&data, vdom, scope);
data
}
/// Get the errors from the suspense boundary
fn serialize_errors(context: &HydrationContext, vdom: &VirtualDom, scope: ScopeId) {
// If there is an error boundary on the suspense boundary, grab the error from the context API
// and throw it on the client so that it bubbles up to the nearest error boundary
let error = vdom.in_runtime(|| {
scope
.consume_context::<ErrorContext>()
.and_then(|error_context| error_context.errors().first().cloned())
});
context
.error_entry()
.insert(&error, std::panic::Location::caller());
}
fn take_from_scope(context: &HydrationContext, vdom: &VirtualDom, scope: ScopeId) {
vdom.in_runtime(|| {
scope.in_runtime(|| {
// Grab any serializable server context from this scope
let other: Option<HydrationContext> = has_context();
if let Some(other) = other {
context.extend(&other);
}
});
});
// then continue to any children
if let Some(scope) = vdom.get_scope(scope) {
// If this is a suspense boundary, move into the children first (even if they are suspended) because that will be run first on the client
if let Some(suspense_boundary) =
SuspenseContext::downcast_suspense_boundary_from_scope(&vdom.runtime(), scope.id())
{
if let Some(node) = suspense_boundary.suspended_nodes() {
take_from_vnode(context, vdom, &node);
}
}
if let Some(node) = scope.try_root_node() {
take_from_vnode(context, vdom, node);
}
}
}
fn take_from_vnode(context: &HydrationContext, vdom: &VirtualDom, vnode: &VNode) {
for (dynamic_node_index, dyn_node) in vnode.dynamic_nodes.iter().enumerate() {
match dyn_node {
DynamicNode::Component(comp) => {
if let Some(scope) = comp.mounted_scope(dynamic_node_index, vnode, vdom) {
take_from_scope(context, vdom, scope.id());
}
}
DynamicNode::Fragment(nodes) => {
for node in nodes {
take_from_vnode(context, vdom, node);
}
}
_ => {}
}
}
}
/// State used in server side rendering. This utilizes a pool of [`dioxus_ssr::Renderer`]s to cache static templates between renders.
#[derive(Clone)]
pub struct SSRState {
// We keep a pool of renderers to avoid re-creating them on every request. They are boxed to make them very cheap to move
renderers: Arc<SsrRendererPool>,
}
impl SSRState {
/// Create a new [`SSRState`].
pub fn new(cfg: &ServeConfig) -> Self {
Self {
renderers: Arc::new(SsrRendererPool::new(4, cfg.incremental.clone())),
}
}
/// Render the application to HTML.
pub async fn render<'a>(
&'a self,
route: String,
cfg: &'a ServeConfig,
virtual_dom_factory: impl FnOnce() -> VirtualDom + Send + Sync + 'static,
server_context: &'a DioxusServerContext,
) -> Result<
(
RenderFreshness,
impl Stream<Item = Result<String, dioxus_isrg::IncrementalRendererError>>,
),
SSRError,
> {
self.renderers
.clone()
.render_to(cfg, route, virtual_dom_factory, server_context)
.await
}
}
/// The template that wraps the body of the HTML for a fullstack page. This template contains the data needed to hydrate server functions that were run on the server.
pub struct FullstackHTMLTemplate {
cfg: ServeConfig,
}
impl FullstackHTMLTemplate {
/// Create a new [`FullstackHTMLTemplate`].
pub fn new(cfg: &ServeConfig) -> Self {
Self { cfg: cfg.clone() }
}
}
impl FullstackHTMLTemplate {
/// Render any content before the head of the page.
pub fn render_head<R: std::fmt::Write>(
&self,
to: &mut R,
virtual_dom: &VirtualDom,
) -> Result<(), dioxus_isrg::IncrementalRendererError> {
let ServeConfig { index, .. } = &self.cfg;
let title = {
let document: Option<std::rc::Rc<ServerDocument>> =
virtual_dom.in_runtime(|| ScopeId::ROOT.consume_context());
// 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<ServerDocument>> =
virtual_dom.in_runtime(|| ScopeId::ROOT.consume_context());
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.
fn render_before_body<R: std::fmt::Write>(
&self,
to: &mut R,
) -> Result<(), dioxus_isrg::IncrementalRendererError> {
let ServeConfig { index, .. } = &self.cfg;
to.write_str(&index.close_head)?;
write!(to, "<script>{INITIALIZE_STREAMING_JS}</script>")?;
Ok(())
}
/// Render all content after the main element of the page.
pub fn render_after_main<R: std::fmt::Write>(
&self,
to: &mut R,
virtual_dom: &VirtualDom,
) -> Result<(), dioxus_isrg::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);
// We always send down the data required to hydrate components on the client
let raw_data = resolved_data.data;
write!(
to,
r#"<script>window.initial_dioxus_hydration_data="{raw_data}";"#,
)?;
#[cfg(debug_assertions)]
{
// In debug mode, we also send down the type names and locations of the serialized data
let debug_types = &resolved_data.debug_types;
let debug_locations = &resolved_data.debug_locations;
write!(
to,
r#"window.initial_dioxus_hydration_debug_types={debug_types};"#,
)?;
write!(
to,
r#"window.initial_dioxus_hydration_debug_locations={debug_locations};"#,
)?;
}
write!(to, r#"</script>"#,)?;
to.write_str(&index.post_main)?;
Ok(())
}
/// Render all content after the body of the page.
pub fn render_after_body<R: std::fmt::Write>(
&self,
to: &mut R,
) -> Result<(), dioxus_isrg::IncrementalRendererError> {
let ServeConfig { index, .. } = &self.cfg;
to.write_str(&index.after_closing_body_tag)?;
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_isrg::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 {
let mut renderer = Renderer::default();
renderer.pre_render = true;
renderer
}
-514
View File
@@ -1,514 +0,0 @@
//! Configuration for how to serve a Dioxus application
#![allow(non_snake_case)]
use dioxus_lib::prelude::dioxus_core::LaunchConfig;
use std::any::Any;
use std::fs::File;
use std::io::Read;
use std::path::PathBuf;
use std::sync::Arc;
#[allow(unused)]
pub(crate) type ContextProviders =
Arc<Vec<Box<dyn Fn() -> Box<dyn std::any::Any> + Send + Sync + 'static>>>;
/// A ServeConfig is used to configure how to serve a Dioxus application. It contains information about how to serve static assets, and what content to render with [`dioxus-ssr`].
#[derive(Clone, Default)]
pub struct ServeConfigBuilder {
pub(crate) root_id: Option<&'static str>,
pub(crate) index_html: Option<String>,
pub(crate) index_path: Option<PathBuf>,
pub(crate) incremental: Option<dioxus_isrg::IncrementalRendererConfig>,
pub(crate) context_providers:
Vec<Arc<dyn Fn() -> Box<dyn std::any::Any> + Send + Sync + 'static>>,
pub(crate) streaming_mode: StreamingMode,
}
impl LaunchConfig for ServeConfigBuilder {}
impl ServeConfigBuilder {
/// Create a new ServeConfigBuilder with incremental static generation disabled and the default index.html settings
pub fn new() -> Self {
Self {
root_id: None,
index_html: None,
index_path: None,
incremental: None,
context_providers: Default::default(),
streaming_mode: StreamingMode::default(),
}
}
/// Enable incremental static generation. Incremental static generation caches the
/// rendered html in memory and/or the file system. It can be used to improve performance of heavy routes.
///
/// ```rust, no_run
/// # fn app() -> Element { todo!() }
/// use dioxus::prelude::*;
///
/// // Finally, launch the app with the config
/// LaunchBuilder::new()
/// // Only set the server config if the server feature is enabled
/// .with_cfg(server_only!(ServeConfigBuilder::default().incremental(IncrementalRendererConfig::default())))
/// .launch(app);
/// ```
pub fn incremental(mut self, cfg: dioxus_isrg::IncrementalRendererConfig) -> Self {
self.incremental = Some(cfg);
self
}
/// Set the contents of the index.html file to be served. (precedence over index_path)
pub fn index_html(mut self, index_html: String) -> Self {
self.index_html = Some(index_html);
self
}
/// Set the path of the index.html file to be served. (defaults to {assets_path}/index.html)
pub fn index_path(mut self, index_path: PathBuf) -> Self {
self.index_path = Some(index_path);
self
}
/// Set the id of the root element in the index.html file to place the prerendered content into. (defaults to main)
///
/// # Example
///
/// If your index.html file looks like this:
/// ```html
/// <!DOCTYPE html>
/// <html>
/// <head>
/// <title>My App</title>
/// </head>
/// <body>
/// <div id="my-custom-root"></div>
/// </body>
/// </html>
/// ```
///
/// You can set the root id to `"my-custom-root"` to render the app into that element:
///
/// ```rust, no_run
/// # fn app() -> Element { todo!() }
/// use dioxus::prelude::*;
///
/// // Finally, launch the app with the config
/// LaunchBuilder::new()
/// // Only set the server config if the server feature is enabled
/// .with_cfg(server_only! {
/// ServeConfigBuilder::default().root_id("app")
/// })
/// // You also need to set the root id in your web config
/// .with_cfg(web! {
/// dioxus::web::Config::default().rootname("app")
/// })
/// // And desktop config
/// .with_cfg(desktop! {
/// dioxus::desktop::Config::default().with_root_name("app")
/// })
/// .launch(app);
/// ```
pub fn root_id(mut self, root_id: &'static str) -> Self {
self.root_id = Some(root_id);
self
}
/// Provide context to the root and server functions. You can use this context while rendering with [`consume_context`](dioxus_lib::prelude::consume_context)
/// or in server functions with [`FromContext`](crate::prelude::FromContext).
///
///
/// The context providers passed into this method will be called when the context type is requested which may happen many times in the lifecycle of the application.
///
///
/// Context will be forwarded from the LaunchBuilder if it is provided.
///
/// ```rust, no_run
/// #![allow(non_snake_case)]
/// use dioxus::prelude::*;
/// use std::sync::Arc;
/// use std::any::Any;
///
/// fn main() {
/// #[cfg(feature = "web")]
/// // Hydrate the application on the client
/// dioxus::launch(app);
/// #[cfg(feature = "server")]
/// {
/// tokio::runtime::Runtime::new()
/// .unwrap()
/// .block_on(async move {
/// let address = dioxus_cli_config::fullstack_address_or_localhost();
/// let listener = tokio::net::TcpListener::bind(address)
/// .await
/// .unwrap();
/// let config = ServeConfigBuilder::default()
/// // You can provide context to your whole app on the server (including server functions) with the `context_provider` method on the launch builder
/// .context_providers(Arc::new(vec![Box::new(|| Box::new(1234u32) as Box<dyn Any>) as Box<dyn Fn() -> Box<dyn std::any::Any> + Send + Sync>]));
/// axum::serve(
/// listener,
/// axum::Router::new()
/// .serve_dioxus_application(config, app)
/// .into_make_service(),
/// )
/// .await
/// .unwrap();
/// });
/// }
/// }
///
/// #[server]
/// async fn read_context() -> Result<u32, ServerFnError> {
/// // You can extract values from the server context with the `extract` function
/// let FromContext(value) = extract().await?;
/// Ok(value)
/// }
///
/// fn app() -> Element {
/// let future = use_resource(read_context);
/// rsx! {
/// h1 { "{future:?}" }
/// }
/// }
/// ```
pub fn context_providers(mut self, state: ContextProviders) -> Self {
// This API should probably accept Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync + 'static>> instead of Arc so we can
// continue adding to the context list after calling this method. Changing the type is a breaking change so it cannot
// be done until 0.7 is released.
let context_providers = (0..state.len()).map(|i| {
let state = state.clone();
Arc::new(move || state[i]()) as Arc<dyn Fn() -> Box<dyn std::any::Any> + Send + Sync>
});
self.context_providers.extend(context_providers);
self
}
/// Provide context to the root and server functions. You can use this context
/// while rendering with [`consume_context`](dioxus_lib::prelude::consume_context) or in server functions with [`FromContext`](crate::prelude::FromContext).
///
///
/// The context providers passed into this method will be called when the context type is requested which may happen many times in the lifecycle of the application.
///
///
/// Context will be forwarded from the LaunchBuilder if it is provided.
///
/// ```rust, no_run
/// #![allow(non_snake_case)]
/// use dioxus::prelude::*;
///
/// fn main() {
/// #[cfg(feature = "web")]
/// // Hydrate the application on the client
/// dioxus::launch(app);
/// #[cfg(feature = "server")]
/// {
/// tokio::runtime::Runtime::new()
/// .unwrap()
/// .block_on(async move {
/// let address = dioxus_cli_config::fullstack_address_or_localhost();
/// let listener = tokio::net::TcpListener::bind(address)
/// .await
/// .unwrap();
/// let config = ServeConfigBuilder::default()
/// // You can provide context to your whole app on the server (including server functions) with the `context_provider` method on the launch builder
/// .context_provider(move || 1234u32);
/// axum::serve(
/// listener,
/// axum::Router::new()
/// .serve_dioxus_application(config, app)
/// .into_make_service(),
/// )
/// .await
/// .unwrap();
/// });
/// }
/// }
///
/// #[server]
/// async fn read_context() -> Result<u32, ServerFnError> {
/// // You can extract values from the server context with the `extract` function
/// let FromContext(value) = extract().await?;
/// Ok(value)
/// }
///
/// fn app() -> Element {
/// let future = use_resource(read_context);
/// rsx! {
/// h1 { "{future:?}" }
/// }
/// }
/// ```
pub fn context_provider<C: 'static>(
mut self,
state: impl Fn() -> C + Send + Sync + 'static,
) -> Self {
self.context_providers
.push(Arc::new(move || Box::new(state())));
self
}
/// Provide context to the root and server functions. You can use this context while rendering with [`consume_context`](dioxus_lib::prelude::consume_context)
/// or in server functions with [`FromContext`](crate::prelude::FromContext).
///
/// Context will be forwarded from the LaunchBuilder if it is provided.
///
/// ```rust, no_run
/// #![allow(non_snake_case)]
/// use dioxus::prelude::*;
///
/// fn main() {
/// #[cfg(feature = "web")]
/// // Hydrate the application on the client
/// dioxus::launch(app);
/// #[cfg(feature = "server")]
/// {
/// tokio::runtime::Runtime::new()
/// .unwrap()
/// .block_on(async move {
/// let address = dioxus_cli_config::fullstack_address_or_localhost();
/// let listener = tokio::net::TcpListener::bind(address)
/// .await
/// .unwrap();
/// let config = ServeConfigBuilder::default()
/// // You can provide context to your whole app on the server (including server functions) with the `context_provider` method on the launch builder
/// .context(1234u32);
/// axum::serve(
/// listener,
/// axum::Router::new()
/// .serve_dioxus_application(config, app)
/// .into_make_service(),
/// )
/// .await
/// .unwrap();
/// });
/// }
/// }
///
/// #[server]
/// async fn read_context() -> Result<u32, ServerFnError> {
/// // You can extract values from the server context with the `extract` function
/// let FromContext(value) = extract().await?;
/// Ok(value)
/// }
///
/// fn app() -> Element {
/// let future = use_resource(read_context);
/// rsx! {
/// h1 { "{future:?}" }
/// }
/// }
/// ```
pub fn context(mut self, state: impl Any + Clone + Send + Sync + 'static) -> Self {
self.context_providers
.push(Arc::new(move || Box::new(state.clone())));
self
}
/// Set the streaming mode for the server. By default, streaming is disabled.
///
/// ```rust, no_run
/// # use dioxus::prelude::*;
/// # fn app() -> Element { todo!() }
/// dioxus::LaunchBuilder::new()
/// .with_context(server_only! {
/// dioxus::fullstack::ServeConfig::builder().streaming_mode(dioxus::fullstack::StreamingMode::OutOfOrder)
/// })
/// .launch(app);
/// ```
pub fn streaming_mode(mut self, mode: StreamingMode) -> Self {
self.streaming_mode = mode;
self
}
/// Enable out of order streaming. This will cause server futures to be resolved out of order and streamed to the client as they resolve.
///
/// It is equivalent to calling `streaming_mode(StreamingMode::OutOfOrder)`
///
/// ```rust, no_run
/// # use dioxus::prelude::*;
/// # fn app() -> Element { todo!() }
/// dioxus::LaunchBuilder::new()
/// .with_context(server_only! {
/// dioxus::fullstack::ServeConfig::builder().enable_out_of_order_streaming()
/// })
/// .launch(app);
/// ```
pub fn enable_out_of_order_streaming(mut self) -> Self {
self.streaming_mode = StreamingMode::OutOfOrder;
self
}
/// Build the ServeConfig. This may fail if the index.html file is not found.
///
/// ## WASM compatibility
/// In the context of WASM the file system normally can't be read so this always requires
/// the `index_html` field to be set.
pub fn build(self) -> Result<ServeConfig, UnableToLoadIndex> {
let root_id = self.root_id.unwrap_or("main");
let index_html = match self.index_html {
Some(index) => index,
None => {
// The CLI always bundles static assets into the exe/public directory
let public_path = public_path();
let index_path = self
.index_path
.unwrap_or_else(|| public_path.join("index.html"));
load_index_path(index_path)?
}
};
let index = load_index_html(index_html, root_id);
let context_providers = Arc::new(
self.context_providers
.into_iter()
.map(|f| {
Box::new(move || f()) as Box<dyn Fn() -> Box<dyn std::any::Any> + Send + Sync>
})
.collect(),
);
Ok(ServeConfig {
index,
incremental: self.incremental,
context_providers,
streaming_mode: self.streaming_mode,
})
}
}
impl TryInto<ServeConfig> for ServeConfigBuilder {
type Error = UnableToLoadIndex;
fn try_into(self) -> Result<ServeConfig, Self::Error> {
self.build()
}
}
/// Get the path to the public assets directory to serve static files from
pub(crate) fn public_path() -> PathBuf {
if let Ok(path) = std::env::var("DIOXUS_PUBLIC_PATH") {
return PathBuf::from(path);
}
// The CLI always bundles static assets into the exe/public directory
std::env::current_exe()
.expect("Failed to get current executable path")
.parent()
.unwrap()
.join("public")
}
/// An error that can occur when loading the index.html file
#[derive(Debug)]
pub struct UnableToLoadIndex(PathBuf);
impl std::fmt::Display for UnableToLoadIndex {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Failed to find index.html. Make sure the index_path is set correctly and the WASM application has been built. Tried to open file at path: {:?}", self.0)
}
}
impl std::error::Error for UnableToLoadIndex {}
fn load_index_path(path: PathBuf) -> Result<String, UnableToLoadIndex> {
let mut file = File::open(&path).map_err(|_| UnableToLoadIndex(path))?;
let mut contents = String::new();
file.read_to_string(&mut contents)
.expect("Failed to read index.html");
Ok(contents)
}
fn load_index_html(contents: String, root_id: &'static str) -> IndexHtml {
let (pre_main, post_main) = contents.split_once(&format!("id=\"{root_id}\"")).unwrap_or_else(|| panic!("Failed to find id=\"{root_id}\" in index.html. The id is used to inject the application into the page."));
let post_main = post_main.split_once('>').unwrap_or_else(|| {
panic!("Failed to find closing > after id=\"{root_id}\" in index.html.")
});
let (pre_main, post_main) = (
pre_main.to_string() + &format!("id=\"{root_id}\"") + post_main.0 + ">",
post_main.1.to_string(),
);
let (head, close_head) = pre_main.split_once("</head>").unwrap_or_else(|| {
panic!("Failed to find closing </head> tag after id=\"{root_id}\" in index.html.")
});
let (head, close_head) = (head.to_string(), "</head>".to_string() + close_head);
let (post_main, after_closing_body_tag) =
post_main.split_once("</body>").unwrap_or_else(|| {
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 {
head_before_title,
head_after_title,
title,
close_head,
post_main: post_main.to_string(),
after_closing_body_tag: "</body>".to_string() + after_closing_body_tag,
}
}
#[derive(Clone)]
pub(crate) struct IndexHtml {
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,
}
/// The streaming mode to use while rendering the page
#[derive(Clone, Copy, Default, PartialEq)]
pub enum StreamingMode {
/// Streaming is disabled; all server futures should be resolved before hydrating the page on the client
#[default]
Disabled,
/// Out of order streaming is enabled; server futures are resolved out of order and streamed to the client
/// as they resolve
OutOfOrder,
}
/// Used to configure how to serve a Dioxus application. It contains information about how to serve static assets, and what content to render with [`dioxus-ssr`].
/// See [`ServeConfigBuilder`] to create a ServeConfig
#[derive(Clone)]
pub struct ServeConfig {
pub(crate) index: IndexHtml,
pub(crate) incremental: Option<dioxus_isrg::IncrementalRendererConfig>,
// This is used in the axum integration
#[allow(unused)]
pub(crate) context_providers: ContextProviders,
pub(crate) streaming_mode: StreamingMode,
}
impl LaunchConfig for ServeConfig {}
impl ServeConfig {
/// Create a new ServeConfig
#[cfg(not(target_family = "wasm"))]
pub fn new() -> Result<Self, UnableToLoadIndex> {
ServeConfigBuilder::new().build()
}
/// Create a new builder for a ServeConfig
pub fn builder() -> ServeConfigBuilder {
ServeConfigBuilder::new()
}
}
-110
View File
@@ -1,110 +0,0 @@
//! A launch function that creates an axum router for the LaunchBuilder
use std::any::Any;
use axum::{
body::Body,
extract::{Request, State},
response::IntoResponse,
};
use dioxus_cli_config::base_path;
use dioxus_lib::prelude::*;
use crate::server::{render_handler, RenderHandleState, SSRState};
/// Launch a fullstack app with the given root component, contexts, and config.
#[allow(unused)]
pub fn launch(
root: fn() -> Element,
contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
platform_config: Vec<Box<dyn Any>>,
) -> ! {
use crate::{ServeConfig, ServeConfigBuilder};
#[cfg(not(target_arch = "wasm32"))]
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async move {
let platform_config = platform_config
.into_iter()
.find_map(|cfg| {
cfg.downcast::<ServeConfig>()
.map(|cfg| Result::Ok(*cfg))
.or_else(|cfg| {
cfg.downcast::<ServeConfigBuilder>()
.map(|builder| builder.build())
})
.ok()
})
.unwrap_or_else(ServeConfig::new);
// Extend the config's context providers with the context providers from the launch builder
let platform_config = platform_config.map(|mut cfg| {
let mut contexts = contexts;
let cfg_context_providers = cfg.context_providers.clone();
for i in 0..cfg_context_providers.len() {
contexts.push(Box::new({
let cfg_context_providers = cfg_context_providers.clone();
move || (cfg_context_providers[i])()
}));
}
cfg.context_providers = std::sync::Arc::new(contexts);
cfg
});
// Get the address the server should run on. If the CLI is running, the CLI proxies fullstack into the main address
// and we use the generated address the CLI gives us
let address = dioxus_cli_config::fullstack_address_or_localhost();
use crate::server::DioxusRouterExt;
struct TryIntoResult(Result<ServeConfig, crate::UnableToLoadIndex>);
impl TryInto<ServeConfig> for TryIntoResult {
type Error = crate::UnableToLoadIndex;
fn try_into(self) -> Result<ServeConfig, Self::Error> {
self.0
}
}
let mut base_path = base_path();
let config = platform_config.as_ref().ok().cloned();
let dioxus_router =
axum::Router::new().serve_dioxus_application(TryIntoResult(platform_config), root);
let mut router;
match base_path.as_deref() {
Some(base_path) => {
let base_path = base_path.trim_matches('/');
// If there is a base path, nest the router under it and serve the root route manually
// Nesting a route in axum only serves /base_path or /base_path/ not both
router = axum::Router::new().nest(&format!("/{base_path}/"), dioxus_router);
async fn root_render_handler(
state: State<RenderHandleState>,
mut request: Request<Body>,
) -> impl IntoResponse {
// The root of the base path always looks like the root from dioxus fullstack
*request.uri_mut() = "/".parse().unwrap();
render_handler(state, request).await
}
if let Some(cfg) = config {
let ssr_state = SSRState::new(&cfg);
router = router.route(
&format!("/{base_path}"),
axum::routing::method_routing::get(root_render_handler).with_state(
RenderHandleState::new(cfg, root).with_ssr_state(ssr_state),
),
)
}
}
None => router = dioxus_router,
}
let router = router.into_make_service();
let listener = tokio::net::TcpListener::bind(address).await.unwrap();
axum::serve(listener, router).await.unwrap();
});
unreachable!("Launching a fullstack app should never return")
}
-199
View File
@@ -1,199 +0,0 @@
//! Dioxus utilities for the [Axum](https://docs.rs/axum/latest/axum/index.html) server framework.
//!
//! # Example
//! ```rust, no_run
//! #![allow(non_snake_case)]
//! use dioxus::prelude::*;
//!
//! fn main() {
//! #[cfg(feature = "web")]
//! // Hydrate the application on the client
//! dioxus::launch(app);
//! #[cfg(feature = "server")]
//! {
//! tokio::runtime::Runtime::new()
//! .unwrap()
//! .block_on(async move {
//! // Get the address the server should run on. If the CLI is running, the CLI proxies fullstack into the main address
//! // and we use the generated address the CLI gives us
//! let address = dioxus::cli_config::fullstack_address_or_localhost();
//! let listener = tokio::net::TcpListener::bind(address)
//! .await
//! .unwrap();
//! axum::serve(
//! listener,
//! axum::Router::new()
//! // Server side render the application, serve static assets, and register server functions
//! .serve_dioxus_application(ServeConfigBuilder::default(), app)
//! .into_make_service(),
//! )
//! .await
//! .unwrap();
//! });
//! }
//! }
//!
//! fn app() -> Element {
//! let mut text = use_signal(|| "...".to_string());
//!
//! rsx! {
//! button {
//! onclick: move |_| async move {
//! if let Ok(data) = get_server_data().await {
//! text.set(data);
//! }
//! },
//! "Run a server function"
//! }
//! "Server said: {text}"
//! }
//! }
//!
//! #[server(GetServerData)]
//! async fn get_server_data() -> Result<String, ServerFnError> {
//! Ok("Hello from the server!".to_string())
//! }
//! ```
pub mod launch;
use axum::routing::*;
use dioxus_lib::prelude::Element;
use crate::prelude::*;
/// A extension trait with utilities for integrating Dioxus with your Axum router.
pub trait DioxusRouterExt<S>: DioxusRouterFnExt<S> {
/// Serves the static WASM for your Dioxus application (except the generated index.html).
///
/// # Example
/// ```rust, no_run
/// # #![allow(non_snake_case)]
/// # use dioxus_lib::prelude::*;
/// # use dioxus_fullstack::prelude::*;
/// #[tokio::main]
/// async fn main() {
/// let addr = dioxus::cli_config::fullstack_address_or_localhost();
/// let router = axum::Router::new()
/// // Server side render the application, serve static assets, and register server functions
/// .serve_static_assets()
/// // Server render the application
/// // ...
/// .into_make_service();
/// let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
/// axum::serve(listener, router).await.unwrap();
/// }
/// ```
fn serve_static_assets(self) -> Self
where
Self: Sized;
/// Serves the Dioxus application. This will serve a complete server side rendered application.
/// This will serve static assets, server render the application, register server functions, and integrate with hot reloading.
///
/// # Example
/// ```rust, no_run
/// # #![allow(non_snake_case)]
/// # use dioxus_lib::prelude::*;
/// # use dioxus_fullstack::prelude::*;
/// #[tokio::main]
/// async fn main() {
/// let addr = dioxus::cli_config::fullstack_address_or_localhost();
/// let router = axum::Router::new()
/// // Server side render the application, serve static assets, and register server functions
/// .serve_dioxus_application(ServeConfig::new().unwrap(), app)
/// .into_make_service();
/// let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
/// axum::serve(listener, router).await.unwrap();
/// }
///
/// fn app() -> Element {
/// rsx! { "Hello World" }
/// }
/// ```
fn serve_dioxus_application<Cfg, Error>(self, cfg: Cfg, app: fn() -> Element) -> Self
where
Cfg: TryInto<ServeConfig, Error = Error>,
Error: std::error::Error,
Self: Sized;
}
impl<S> DioxusRouterExt<S> for Router<S>
where
S: Send + Sync + Clone + 'static,
{
fn serve_static_assets(mut self) -> Self {
use tower_http::services::{ServeDir, ServeFile};
let public_path = crate::public_path();
if !public_path.exists() {
return self;
}
// Serve all files in public folder except index.html
let dir = std::fs::read_dir(&public_path).unwrap_or_else(|e| {
panic!(
"Couldn't read public directory at {:?}: {}",
&public_path, e
)
});
for entry in dir.flatten() {
let path = entry.path();
if path.ends_with("index.html") {
continue;
}
let route = path
.strip_prefix(&public_path)
.unwrap()
.iter()
.map(|segment| {
segment.to_str().unwrap_or_else(|| {
panic!("Failed to convert path segment {:?} to string", segment)
})
})
.collect::<Vec<_>>()
.join("/");
let route = format!("/{}", route);
if path.is_dir() {
self = self.nest_service(&route, ServeDir::new(path).precompressed_br());
} else {
self = self.nest_service(&route, ServeFile::new(path).precompressed_br());
}
}
self
}
fn serve_dioxus_application<Cfg, Error>(self, cfg: Cfg, app: fn() -> Element) -> Self
where
Cfg: TryInto<ServeConfig, Error = Error>,
Error: std::error::Error,
{
let cfg = cfg.try_into();
let context_providers = cfg
.as_ref()
.map(|cfg| cfg.context_providers.clone())
.unwrap_or_default();
// Add server functions and render index.html
let server = self
.serve_static_assets()
.register_server_functions_with_context(context_providers);
match cfg {
Ok(cfg) => {
let ssr_state = SSRState::new(&cfg);
server.fallback(
get(render_handler)
.with_state(RenderHandleState::new(cfg, app).with_ssr_state(ssr_state)),
)
}
Err(err) => {
tracing::trace!("Failed to create render handler. This is expected if you are only using fullstack for desktop/mobile server functions: {}", err);
server
}
}
}
}
-444
View File
@@ -1,444 +0,0 @@
use parking_lot::RwLock;
use std::any::Any;
use std::collections::HashMap;
use std::sync::Arc;
type SendSyncAnyMap = std::collections::HashMap<std::any::TypeId, ContextType>;
/// A shared context for server functions that contains information about the request and middleware state.
///
/// You should not construct this directly inside components or server functions. Instead use [`server_context()`] to get the server context from the current request.
///
/// # Example
///
/// ```rust, no_run
/// # use dioxus::prelude::*;
/// #[server]
/// async fn read_headers() -> Result<(), ServerFnError> {
/// let server_context = server_context();
/// let headers: http::HeaderMap = server_context.extract().await?;
/// println!("{:?}", headers);
/// Ok(())
/// }
/// ```
#[derive(Clone)]
pub struct DioxusServerContext {
shared_context: Arc<RwLock<SendSyncAnyMap>>,
response_parts: Arc<RwLock<http::response::Parts>>,
pub(crate) parts: Arc<RwLock<http::request::Parts>>,
response_sent: Arc<std::sync::atomic::AtomicBool>,
}
enum ContextType {
Factory(Box<dyn Fn() -> Box<dyn Any> + Send + Sync>),
Value(Box<dyn Any + Send + Sync>),
}
impl ContextType {
fn downcast<T: Clone + 'static>(&self) -> Option<T> {
match self {
ContextType::Value(value) => value.downcast_ref::<T>().cloned(),
ContextType::Factory(factory) => factory().downcast::<T>().ok().map(|v| *v),
}
}
}
#[allow(clippy::derivable_impls)]
impl Default for DioxusServerContext {
fn default() -> Self {
Self {
shared_context: Arc::new(RwLock::new(HashMap::new())),
response_parts: Arc::new(RwLock::new(
http::response::Response::new(()).into_parts().0,
)),
parts: Arc::new(RwLock::new(http::request::Request::new(()).into_parts().0)),
response_sent: Arc::new(std::sync::atomic::AtomicBool::new(false)),
}
}
}
mod server_fn_impl {
use super::*;
use parking_lot::{RwLockReadGuard, RwLockWriteGuard};
use std::any::{Any, TypeId};
impl DioxusServerContext {
/// Create a new server context from a request
pub fn new(parts: http::request::Parts) -> Self {
Self {
parts: Arc::new(RwLock::new(parts)),
shared_context: Arc::new(RwLock::new(SendSyncAnyMap::new())),
response_parts: std::sync::Arc::new(RwLock::new(
http::response::Response::new(()).into_parts().0,
)),
response_sent: Arc::new(std::sync::atomic::AtomicBool::new(false)),
}
}
/// Create a server context from a shared parts
#[allow(unused)]
pub(crate) fn from_shared_parts(parts: Arc<RwLock<http::request::Parts>>) -> Self {
Self {
parts,
shared_context: Arc::new(RwLock::new(SendSyncAnyMap::new())),
response_parts: std::sync::Arc::new(RwLock::new(
http::response::Response::new(()).into_parts().0,
)),
response_sent: Arc::new(std::sync::atomic::AtomicBool::new(false)),
}
}
/// Clone a value from the shared server context. If you are using [`DioxusRouterExt`](crate::prelude::DioxusRouterExt), any values you insert into
/// the launch context will also be available in the server context.
///
/// Example:
/// ```rust, no_run
/// use dioxus::prelude::*;
///
/// LaunchBuilder::new()
/// // You can provide context to your whole app (including server functions) with the `with_context` method on the launch builder
/// .with_context(server_only! {
/// 1234567890u32
/// })
/// .launch(app);
///
/// #[server]
/// async fn read_context() -> Result<u32, ServerFnError> {
/// // You can extract values from the server context with the `extract` function
/// let FromContext(value) = extract().await?;
/// Ok(value)
/// }
///
/// fn app() -> Element {
/// let future = use_resource(read_context);
/// rsx! {
/// h1 { "{future:?}" }
/// }
/// }
/// ```
pub fn get<T: Any + Send + Sync + Clone + 'static>(&self) -> Option<T> {
self.shared_context
.read()
.get(&TypeId::of::<T>())
.map(|v| v.downcast::<T>().unwrap())
}
/// Insert a value into the shared server context
pub fn insert<T: Any + Send + Sync + 'static>(&self, value: T) {
self.insert_any(Box::new(value));
}
/// Insert a boxed `Any` value into the shared server context
pub fn insert_any(&self, value: Box<dyn Any + Send + Sync + 'static>) {
self.shared_context
.write()
.insert((*value).type_id(), ContextType::Value(value));
}
/// Insert a factory that creates a non-sync value for the shared server context
pub fn insert_factory<F, T>(&self, value: F)
where
F: Fn() -> T + Send + Sync + 'static,
T: 'static,
{
self.shared_context.write().insert(
TypeId::of::<T>(),
ContextType::Factory(Box::new(move || Box::new(value()))),
);
}
/// Insert a boxed factory that creates a non-sync value for the shared server context
pub fn insert_boxed_factory(&self, value: Box<dyn Fn() -> Box<dyn Any> + Send + Sync>) {
self.shared_context
.write()
.insert((*value()).type_id(), ContextType::Factory(value));
}
/// Get the response parts from the server context
///
#[doc = include_str!("../docs/request_origin.md")]
///
/// # Example
///
/// ```rust, no_run
/// # use dioxus::prelude::*;
/// #[server]
/// async fn set_headers() -> Result<(), ServerFnError> {
/// let server_context = server_context();
/// let response_parts = server_context.response_parts();
/// let cookies = response_parts
/// .headers
/// .get("Cookie")
/// .ok_or_else(|| ServerFnError::new("failed to find Cookie header in the response"))?;
/// println!("{:?}", cookies);
/// Ok(())
/// }
/// ```
pub fn response_parts(&self) -> RwLockReadGuard<'_, http::response::Parts> {
self.response_parts.read()
}
/// Get the response parts from the server context
///
#[doc = include_str!("../docs/request_origin.md")]
///
/// # Example
///
/// ```rust, no_run
/// # use dioxus::prelude::*;
/// #[server]
/// async fn set_headers() -> Result<(), ServerFnError> {
/// let server_context = server_context();
/// server_context.response_parts_mut()
/// .headers
/// .insert("Cookie", http::HeaderValue::from_static("dioxus=fullstack"));
/// Ok(())
/// }
/// ```
pub fn response_parts_mut(&self) -> RwLockWriteGuard<'_, http::response::Parts> {
if self
.response_sent
.load(std::sync::atomic::Ordering::Relaxed)
{
tracing::error!("Attempted to modify the request after the first frame of the response has already been sent. \
You can read the response, but modifying the response will not change the response that the client has already received. \
Try modifying the response before the suspense boundary above the router is resolved.");
}
self.response_parts.write()
}
/// Get the request parts
///
#[doc = include_str!("../docs/request_origin.md")]
///
/// # Example
///
/// ```rust, no_run
/// # use dioxus::prelude::*;
/// #[server]
/// async fn read_headers() -> Result<(), ServerFnError> {
/// let server_context = server_context();
/// let request_parts = server_context.request_parts();
/// let id: &i32 = request_parts
/// .extensions
/// .get()
/// .ok_or_else(|| ServerFnError::new("failed to find i32 extension in the request"))?;
/// println!("{:?}", id);
/// Ok(())
/// }
/// ```
pub fn request_parts(&self) -> parking_lot::RwLockReadGuard<'_, http::request::Parts> {
self.parts.read()
}
/// Get the request parts mutably
///
#[doc = include_str!("../docs/request_origin.md")]
///
/// # Example
///
/// ```rust, no_run
/// # use dioxus::prelude::*;
/// #[server]
/// async fn read_headers() -> Result<(), ServerFnError> {
/// let server_context = server_context();
/// let id: i32 = server_context.request_parts_mut()
/// .extensions
/// .remove()
/// .ok_or_else(|| ServerFnError::new("failed to find i32 extension in the request"))?;
/// println!("{:?}", id);
/// Ok(())
/// }
/// ```
pub fn request_parts_mut(&self) -> parking_lot::RwLockWriteGuard<'_, http::request::Parts> {
self.parts.write()
}
/// Extract part of the request.
///
#[doc = include_str!("../docs/request_origin.md")]
///
/// # Example
///
/// ```rust, no_run
/// # use dioxus::prelude::*;
/// #[server]
/// async fn read_headers() -> Result<(), ServerFnError> {
/// let server_context = server_context();
/// let headers: http::HeaderMap = server_context.extract().await?;
/// println!("{:?}", headers);
/// Ok(())
/// }
/// ```
pub async fn extract<M, T: FromServerContext<M>>(&self) -> Result<T, T::Rejection> {
T::from_request(self).await
}
/// Copy the response parts to a response and mark this server context as sent
#[cfg(feature = "axum")]
pub(crate) fn send_response<B>(&self, response: &mut http::response::Response<B>) {
self.response_sent
.store(true, std::sync::atomic::Ordering::Relaxed);
let parts = self.response_parts.read();
let mut_headers = response.headers_mut();
for (key, value) in parts.headers.iter() {
mut_headers.insert(key, value.clone());
}
*response.status_mut() = parts.status;
*response.version_mut() = parts.version;
response.extensions_mut().extend(parts.extensions.clone());
}
}
}
#[test]
fn server_context_as_any_map() {
let parts = http::Request::new(()).into_parts().0;
let server_context = DioxusServerContext::new(parts);
server_context.insert_boxed_factory(Box::new(|| Box::new(1234u32)));
assert_eq!(server_context.get::<u32>().unwrap(), 1234u32);
}
std::thread_local! {
pub(crate) static SERVER_CONTEXT: std::cell::RefCell<Box<DioxusServerContext>> = Default::default();
}
/// Get information about the current server request.
///
/// This function will only provide the current server context if it is called from a server function or on the server rendering a request.
pub fn server_context() -> DioxusServerContext {
SERVER_CONTEXT.with(|ctx| *ctx.borrow().clone())
}
/// Extract some part from the current server request.
///
/// This function will only provide the current server context if it is called from a server function or on the server rendering a request.
pub async fn extract<E: FromServerContext<I>, I>() -> Result<E, E::Rejection> {
E::from_request(&server_context()).await
}
/// Run a function inside of the server context.
pub fn with_server_context<O>(context: DioxusServerContext, f: impl FnOnce() -> O) -> O {
// before polling the future, we need to set the context
let prev_context = SERVER_CONTEXT.with(|ctx| ctx.replace(Box::new(context)));
// poll the future, which may call server_context()
let result = f();
// after polling the future, we need to restore the context
SERVER_CONTEXT.with(|ctx| ctx.replace(prev_context));
result
}
/// A future that provides the server context to the inner future
#[pin_project::pin_project]
pub struct ProvideServerContext<F: std::future::Future> {
context: DioxusServerContext,
#[pin]
f: F,
}
impl<F: std::future::Future> ProvideServerContext<F> {
/// Create a new future that provides the server context to the inner future
pub fn new(f: F, context: DioxusServerContext) -> Self {
Self { f, context }
}
}
impl<F: std::future::Future> std::future::Future for ProvideServerContext<F> {
type Output = F::Output;
fn poll(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Self::Output> {
let this = self.project();
let context = this.context.clone();
with_server_context(context, || this.f.poll(cx))
}
}
/// A trait for extracting types from the server context
#[async_trait::async_trait]
pub trait FromServerContext<I = ()>: Sized {
/// The error type returned when extraction fails. This type must implement `std::error::Error`.
type Rejection;
/// Extract this type from the server context.
async fn from_request(req: &DioxusServerContext) -> Result<Self, Self::Rejection>;
}
/// A type was not found in the server context
pub struct NotFoundInServerContext<T: 'static>(std::marker::PhantomData<T>);
impl<T: 'static> std::fmt::Debug for NotFoundInServerContext<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let type_name = std::any::type_name::<T>();
write!(f, "`{type_name}` not found in server context")
}
}
impl<T: 'static> std::fmt::Display for NotFoundInServerContext<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let type_name = std::any::type_name::<T>();
write!(f, "`{type_name}` not found in server context")
}
}
impl<T: 'static> std::error::Error for NotFoundInServerContext<T> {}
/// Extract a value from the server context provided through the launch builder context or [`DioxusServerContext::insert`]
///
/// Example:
/// ```rust, no_run
/// use dioxus::prelude::*;
///
/// dioxus::LaunchBuilder::new()
/// // You can provide context to your whole app (including server functions) with the `with_context` method on the launch builder
/// .with_context(server_only! {
/// 1234567890u32
/// })
/// .launch(app);
///
/// #[server]
/// async fn read_context() -> Result<u32, ServerFnError> {
/// // You can extract values from the server context with the `extract` function
/// let FromContext(value) = extract().await?;
/// Ok(value)
/// }
///
/// fn app() -> Element {
/// let future = use_resource(read_context);
/// rsx! {
/// h1 { "{future:?}" }
/// }
/// }
/// ```
pub struct FromContext<T: std::marker::Send + std::marker::Sync + Clone + 'static>(pub T);
#[async_trait::async_trait]
impl<T: Send + Sync + Clone + 'static> FromServerContext for FromContext<T> {
type Rejection = NotFoundInServerContext<T>;
async fn from_request(req: &DioxusServerContext) -> Result<Self, Self::Rejection> {
Ok(Self(req.get::<T>().ok_or({
NotFoundInServerContext::<T>(std::marker::PhantomData::<T>)
})?))
}
}
#[cfg(feature = "axum")]
#[cfg_attr(docsrs, doc(cfg(feature = "axum")))]
/// An adapter for axum extractors for the server context
pub struct Axum;
#[cfg(feature = "axum")]
#[async_trait::async_trait]
impl<I: axum::extract::FromRequestParts<()>> FromServerContext<Axum> for I {
type Rejection = I::Rejection;
#[allow(clippy::all)]
async fn from_request(req: &DioxusServerContext) -> Result<Self, Self::Rejection> {
let mut lock = req.request_parts_mut();
I::from_request_parts(&mut lock, &()).await
}
}
-162
View File
@@ -1,162 +0,0 @@
//! There are two common ways to render suspense:
//! 1. Stream the HTML in order - this will work even if javascript is disabled, but if there is something slow at the top of your page, and fast at the bottom, nothing will render until the slow part is done
//! 2. Render placeholders and stream the HTML out of order - this will only work if javascript is enabled. This lets you render any parts of your page that resolve quickly, and then render the rest of the page as it becomes available
//!
//! Dioxus currently uses a the second out of order streaming approach which requires javascript. The rendering structure is as follows:
//! ```html
//! // Initial content is sent down with placeholders
//! <div>
//! Header
//! <div class="flex flex-col">
//! // If we reach a suspense placeholder that may be replaced later, we insert a template node with a unique id to replace later
//! <div>Loading user info...</div>
//! </div>
//! Footer
//! </div>
//! // After the initial render is done, we insert divs that are hidden with new content.
//! // We use divs instead of templates for better SEO
//! <script>
//! // Code to hook up hydration replacement
//! </script>
//! <div hidden id="ds-1-r">
//! <div>Final HTML</div>
//! </div>
//! <script>
//! window.dx_hydrate(2, "suspenseboundarydata");
//! </script>
//! ```
use dioxus_fullstack_protocol::SerializedHydrationData;
use futures_channel::mpsc::Sender;
use std::{
fmt::{Display, Write},
sync::{Arc, RwLock},
};
/// Sections are identified by a unique id based on the suspense path. We only track the path of suspense boundaries because the client may render different components than the server.
#[derive(Clone, Debug, Default)]
struct MountPath {
parent: Option<Arc<MountPath>>,
id: usize,
}
impl MountPath {
fn child(&self) -> Self {
Self {
parent: Some(Arc::new(self.clone())),
id: 0,
}
}
}
impl Display for MountPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(parent) = &self.parent {
write!(f, "{},", parent)?;
}
write!(f, "{}", self.id)
}
}
pub(crate) struct StreamingRenderer<E = std::convert::Infallible> {
channel: RwLock<Sender<Result<String, E>>>,
current_path: RwLock<MountPath>,
}
impl<E> StreamingRenderer<E> {
/// Create a new streaming renderer with the given head that renders into a channel
pub(crate) fn new(
before_body: impl Display,
mut render_into: Sender<Result<String, E>>,
) -> Self {
let start_html = before_body.to_string();
_ = render_into.start_send(Ok(start_html));
Self {
channel: render_into.into(),
current_path: Default::default(),
}
}
/// Render a new chunk of html that will never change
pub(crate) fn render(&self, html: impl Display) {
_ = self
.channel
.write()
.unwrap()
.start_send(Ok(html.to_string()));
}
/// Render a new chunk of html that may change
pub(crate) fn render_placeholder<W: Write + ?Sized>(
&self,
html: impl FnOnce(&mut W) -> std::fmt::Result,
into: &mut W,
) -> Result<Mount, std::fmt::Error> {
let id = self.current_path.read().unwrap().clone();
// Increment the id for the next placeholder
self.current_path.write().unwrap().id += 1;
// While we are inside the placeholder, set the suspense path to the suspense boundary that we are rendering
let old_path = std::mem::replace(&mut *self.current_path.write().unwrap(), id.child());
html(into)?;
// Restore the old path
*self.current_path.write().unwrap() = old_path;
Ok(Mount { id })
}
/// Replace a placeholder that was rendered previously
pub(crate) fn replace_placeholder<W: Write + ?Sized>(
&self,
id: Mount,
html: impl FnOnce(&mut W) -> std::fmt::Result,
resolved_data: SerializedHydrationData,
into: &mut W,
) -> std::fmt::Result {
// Then replace the suspense placeholder with the new content
write!(into, r#"<div id="ds-{id}-r" hidden>"#)?;
// While we are inside the placeholder, set the suspense path to the suspense boundary that we are rendering
let old_path = std::mem::replace(&mut *self.current_path.write().unwrap(), id.id.child());
html(into)?;
// Restore the old path
*self.current_path.write().unwrap() = old_path;
// dx_hydrate accepts 2-4 arguments. The first two are required, the rest are optional
// The arguments are:
// 1. The id of the nodes we are hydrating under
// 2. The serialized data required to hydrate those components
// 3. (in debug mode) The type names of the serialized data
// 4. (in debug mode) The locations of the serialized data
write!(
into,
r#"</div><script>window.dx_hydrate([{id}], "{}""#,
resolved_data.data
)?;
#[cfg(debug_assertions)]
{
// In debug mode, we also send down the type names and locations of the serialized data
let debug_types = &resolved_data.debug_types;
let debug_locations = &resolved_data.debug_locations;
write!(into, r#", {debug_types}, {debug_locations}"#,)?;
}
write!(into, r#")</script>"#)?;
Ok(())
}
/// Close the stream with an error
pub(crate) fn close_with_error(&self, error: E) {
_ = self.channel.write().unwrap().start_send(Err(error));
}
}
/// A mounted placeholder in the dom that may change in the future
#[derive(Clone, Debug)]
pub(crate) struct Mount {
id: MountPath,
}
impl Display for Mount {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.id)
}
}
@@ -1,13 +1,10 @@
#![allow(unused)]
//! On the client, we use the [`WebDocument`] implementation to render the head for any elements that were not rendered on the server.
use dioxus_lib::{document::*, prelude::queue_effect};
use dioxus_lib::document::*;
use dioxus_web::WebDocument;
use super::head_element_hydration_entry;
fn head_element_written_on_server() -> bool {
head_element_hydration_entry()
dioxus_fullstack_protocol::head_element_hydration_entry()
.get()
.ok()
.unwrap_or_default()