more fullstack cleanups: loaders serialize, ServeConfig only (#4785)

* some opinionated cleanups in dioxus-server

* add serialize to loader

* add `result` to Resource

* clippy

* fix

* writable memos, resources, `onmount`, `.pending`, and `.value()` on actions

* fix compile issues

* fix typo
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2025-10-14 17:34:52 -07:00
1 parent a3e40a5ea2
commit 7102bc3b6a
33 files changed
+2085 -672

No files matched your search

+77 -86
View File
@@ -2,20 +2,17 @@ use crate::{
ssr::{SSRError, SsrRendererPool},
ServeConfig, ServerFunction,
};
use crate::{IncrementalRendererError, RenderFreshness};
use axum::{
body,
body::Body,
extract::State,
http::{Request, Response, StatusCode},
http::{Request, StatusCode},
response::IntoResponse,
response::Response,
routing::*,
};
use dioxus_core::{Element, VirtualDom};
use dioxus_fullstack_core::HttpError;
use futures::Stream;
use http::{header::*, request::Parts};
use std::path::Path;
use http::header::*;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tower::util::MapResponse;
use tower::ServiceExt;
@@ -46,10 +43,9 @@ use tower_http::services::fs::ServeFileSystemResponseBody;
/// // 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();
/// .fallback(get(render_handler))
/// .with_state(RenderHandleState::new(ServeConfig::new(), app));
///
/// let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
/// axum::serve(listener, router).await.unwrap();
/// }
@@ -103,7 +99,7 @@ pub trait DioxusRouterExt<S>: DioxusRouterFnExt<S> {
/// 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)
/// .serve_dioxus_application(ServeConfig::new(), app)
/// .into_make_service();
/// let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
/// axum::serve(listener, router).await.unwrap();
@@ -124,7 +120,7 @@ where
S: Send + Sync + Clone + 'static,
{
fn serve_static_assets(self) -> Self {
let public_path = crate::public_path();
let public_path = public_path();
if !public_path.exists() {
return self;
@@ -166,6 +162,7 @@ pub trait DioxusRouterFnExt<S> {
fn register_server_functions(self) -> Self
where
Self: Sized;
/// Serves a Dioxus application without static assets.
/// Sets up server function routes and rendering endpoints only.
///
@@ -179,7 +176,7 @@ pub trait DioxusRouterFnExt<S> {
/// #[tokio::main]
/// async fn main() {
/// let router = axum::Router::new()
/// .serve_api_application(ServeConfig::new().unwrap(), app)
/// .serve_api_application(ServeConfig::new(), app)
/// .into_make_service();
/// // ...
/// }
@@ -195,8 +192,14 @@ pub trait DioxusRouterFnExt<S> {
impl<S: Send + Sync + Clone + 'static> DioxusRouterFnExt<S> for Router<S> {
fn register_server_functions(mut self) -> Self {
for f in ServerFunction::collect() {
self = f.register_server_fn_on_router(self);
for func in ServerFunction::collect() {
tracing::info!(
"Registering server function: {} {}",
func.method(),
func.path()
);
self = func.register_server_fn_on_router(self);
}
self
}
@@ -229,7 +232,8 @@ impl RenderHandleState {
}
}
/// Create a new [`RenderHandleState`] with a custom [`VirtualDom`] factory. This method can be used to pass context into the root component of your application.
/// 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,
@@ -272,36 +276,36 @@ impl RenderHandleState {
/// // 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))
/// )
/// .fallback(get(render_handler))
/// .with_state(RenderHandleState::new(ServeConfig::new(), 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<Self>,
request: Request<Body>,
) -> impl IntoResponse {
let cfg = &state.config;
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
}
};
pub async fn render_handler(State(state): State<Self>, request: Request<Body>) -> Response {
let (parts, _) = request.into_parts();
match state.render(parts, cfg, build_virtual_dom).await {
let response = state
.renderers
.clone()
.render_to(parts, &state.config, {
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
}
})
.await;
match response {
Ok((status, freshness, rx)) => {
let mut response = axum::response::Response::builder()
let mut response = Response::builder()
.status(status.status)
.header(CONTENT_TYPE, "text/html; charset=utf-8")
.body(Body::from_stream(rx))
@@ -309,15 +313,20 @@ impl RenderHandleState {
freshness.write(response.headers_mut());
Result::<http::Response<axum::body::Body>, StatusCode>::Ok(response)
response
}
Err(SSRError::Incremental(e)) => {
tracing::error!("Failed to render page: {}", e);
Ok(Self::err_to_axum_response(e).into_response())
Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(e.to_string())
.unwrap()
.into_response()
}
Err(SSRError::HttpError { status, message }) => Ok(Response::builder()
Err(SSRError::HttpError { status, message }) => Response::builder()
.status(status)
.body(Body::from(message.unwrap_or_else(|| {
status
@@ -325,43 +334,26 @@ impl RenderHandleState {
.unwrap_or("An unknown error occurred")
.to_string()
})))
.unwrap()),
.unwrap(),
}
}
fn err_to_axum_response<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()
}
/// Render the application to HTML.
pub async fn render<'a>(
&'a self,
parts: Parts,
cfg: &'a ServeConfig,
virtual_dom_factory: impl FnOnce() -> VirtualDom + Send + Sync + 'static,
) -> Result<
(
HttpError,
RenderFreshness,
impl Stream<Item = Result<String, IncrementalRendererError>>,
),
SSRError,
> {
self.renderers
.clone()
.render_to(parts, cfg, virtual_dom_factory)
.await
}
}
fn serve_dir_cached<S>(
mut router: Router<S>,
public_path: &std::path::Path,
directory: &std::path::Path,
) -> Router<S>
/// 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")
}
fn serve_dir_cached<S>(mut router: Router<S>, public_path: &Path, directory: &Path) -> Router<S>
where
S: Send + Sync + Clone + 'static,
{
@@ -376,8 +368,16 @@ where
if path == public_path.join("index.html") {
continue;
}
let route = path.strip_prefix(public_path).unwrap();
let route = path_components_to_route_lossy(route);
let route = format!(
"/{}",
path.strip_prefix(public_path)
.unwrap()
.iter()
.map(|segment| segment.to_string_lossy())
.collect::<Vec<_>>()
.join("/")
);
if path.is_dir() {
router = serve_dir_cached(router, public_path, &path);
@@ -397,15 +397,6 @@ where
router
}
fn path_components_to_route_lossy(path: &Path) -> String {
let route = path
.iter()
.map(|segment| segment.to_string_lossy())
.collect::<Vec<_>>()
.join("/");
format!("/{}", route)
}
type MappedAxumService<S> = MapResponse<
S,
fn(Response<ServeFileSystemResponseBody>) -> Response<ServeFileSystemResponseBody>,