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

+80 -68
View File
@@ -1,11 +1,13 @@
//! A launch function that creates an axum router for the LaunchBuilder
use crate::{server::DioxusRouterExt, RenderHandleState, ServeConfig, ServeConfigBuilder};
use crate::IndexHtml;
use crate::{server::DioxusRouterExt, RenderHandleState, ServeConfig};
use anyhow::Context;
use axum::{
body::Body,
extract::{Request, State},
response::IntoResponse,
routing::IntoMakeService,
Router,
};
use dioxus_cli_config::base_path;
use dioxus_core::Element;
@@ -19,11 +21,11 @@ use hyper_util::{
rt::{TokioExecutor, TokioIo},
service::TowerToHyperService,
};
use std::sync::Arc;
use std::{any::Any, collections::HashMap, net::SocketAddr, prelude::rust_2024::Future};
use tokio::net::TcpStream;
use tokio_util::task::LocalPoolHandle;
use tower::Service;
use tower::ServiceExt as _;
use tower::{Service, ServiceExt as _};
type ContextList = Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>;
@@ -55,34 +57,32 @@ async fn serve_server(
dioxus_devtools::connect(move |msg| _ = devtools_tx.unbounded_send(msg));
let platform_config = platform_config
let mut cfg = 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()
})
.find_map(|cfg| cfg.downcast::<ServeConfig>().ok().map(|b| *b))
.unwrap_or_else(ServeConfig::new);
// Extend the config's context providers with the context providers from the launch builder
let cfg = 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
})
.unwrap();
for ctx in contexts {
let arced = Arc::new(ctx) as Arc<dyn Fn() -> Box<dyn Any> + Send + Sync>;
cfg.context_providers.push(arced);
}
// Create the IndexHtml object if the user has specified an override
if let Some(raw_index_html) = cfg.index_html_override.as_ref() {
cfg.index = IndexHtml::new(raw_index_html, cfg.root_id.unwrap_or("main"))
.expect("Failed to parse custom index.html");
} else if !cfg!(target_arch = "wasm32") {
let public_path = crate::public_path();
let index_html_path = public_path.join("index.html");
if index_html_path.exists() {
let index_html = std::fs::read_to_string(index_html_path)
.expect("Failed to read index.html from public directory");
cfg.index = IndexHtml::new(&index_html, cfg.root_id.unwrap_or("main"))
.expect("Failed to parse index.html from public directory");
}
}
// 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
@@ -90,7 +90,7 @@ async fn serve_server(
// Create the router and register the server functions under the basepath.
let router = apply_base_path(
axum::Router::new().serve_dioxus_application(cfg.clone(), original_root),
Router::new().serve_dioxus_application(cfg.clone(), original_root),
original_root,
cfg.clone(),
base_path().map(|s| s.to_string()),
@@ -101,6 +101,7 @@ async fn serve_server(
.map(usize::from)
.unwrap_or(1),
);
let mut make_service = router.into_make_service();
let listener = tokio::net::TcpListener::bind(address).await.unwrap();
@@ -136,7 +137,7 @@ async fn serve_server(
let tcp_stream = TokioIo::new(tcp_stream);
std::future::poll_fn(|cx| {
<IntoMakeService<axum::Router> as tower::Service<Request>>::poll_ready(
<IntoMakeService<Router> as tower::Service<Request>>::poll_ready(
&mut make_service,
cx,
)
@@ -189,8 +190,8 @@ async fn serve_server(
unsafe { dioxus_devtools::subsecond::apply_patch(table).unwrap() };
let mut new_router = axum::Router::new().serve_static_assets();
let new_cfg = ServeConfig::new().unwrap();
let mut new_router = Router::new().serve_static_assets();
let new_cfg = ServeConfig::new();
let server_fn_iter = ServerFunction::collect();
@@ -250,58 +251,69 @@ async fn serve_server(
}
fn apply_base_path(
mut router: axum::Router,
root: fn() -> Result<VNode, RenderError>,
mut router: Router,
root: fn() -> Element,
cfg: ServeConfig,
base_path: Option<String>,
) -> axum::Router {
) -> Router {
if let Some(base_path) = 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}/"), 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();
RenderHandleState::render_handler(state, request).await
}
router = router.route(
router = Router::new().nest(&format!("/{base_path}/"), router).route(
&format!("/{base_path}"),
axum::routing::method_routing::get(root_render_handler)
.with_state(RenderHandleState::new(cfg, root)),
axum::routing::method_routing::get(
|state: State<RenderHandleState>, mut request: Request<Body>| async move {
// The root of the base path always looks like the root from dioxus fullstack
*request.uri_mut() = "/".parse().unwrap();
RenderHandleState::render_handler(state, request).await
},
)
.with_state(RenderHandleState::new(cfg, root)),
)
}
router
}
pub fn serve<F>(mut serve_it: impl FnMut() -> F)
/// Serve a fullstack dioxus application with a custom axum router.
///
/// This function sets up an async runtime, enables the default dioxus logger, runs the provided initializer,
/// and then starts an axum server with the returned router.
///
/// The axum router will be bound to the address specified by the `IP` and `PORT` environment variables,
/// defaulting to `127.0.0.1:8080` if not set.
pub fn serve<F>(mut serve_it: impl FnMut() -> F) -> !
where
F: Future<Output = Result<axum::Router, anyhow::Error>>,
F: Future<Output = Result<Router, anyhow::Error>>,
{
dioxus_logger::initialize_default();
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async move {
let router = serve_it().await.unwrap();
let app_future = async move {
let router = serve_it().await.expect("Failed to create axum router");
let address = dioxus_cli_config::fullstack_address_or_localhost();
let listener = tokio::net::TcpListener::bind(address)
.await
.with_context(|| format!("Failed to bind app to given address: {address}"))
.unwrap();
tracing::trace!("Listening on {address}");
axum::serve::serve(listener, router)
.await
.expect("Failed to serve axum app");
};
let address = dioxus_cli_config::fullstack_address_or_localhost();
let listener = tokio::net::TcpListener::bind(address).await.unwrap();
if let Ok(handle) = tokio::runtime::Handle::try_current() {
handle.block_on(app_future);
} else {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
.block_on(app_future);
}
tracing::trace!("Listening on {address}");
axum::serve::serve(listener, router.into_make_service())
.await
.unwrap();
});
// unreachable!("Serving a fullstack app should never return")
unreachable!("Serving a fullstack app should never return")
}
/// Create a router that serves the dioxus application at the appropriate base path.
@@ -315,10 +327,10 @@ where
/// - Hot-reloading
/// - Async Runtime
/// - Logging
pub fn router(app: fn() -> Element) -> axum::Router {
let cfg = ServeConfig::new().unwrap();
pub fn router(app: fn() -> Element) -> Router {
let cfg = ServeConfig::new();
apply_base_path(
axum::Router::new().serve_dioxus_application(cfg.clone(), app),
Router::new().serve_dioxus_application(cfg.clone(), app),
app,
cfg,
base_path().map(|s| s.to_string()),