Upgrade to axum 0.7, drop 3rd party adapters

This commit is contained in:
Jonathan Kelley committed 2024-02-15 18:05:30 -08:00
1 parent 5dd33c9dcd
commit 214a907adc
53 files changed
+643 -3260

No files matched your search

@@ -55,7 +55,7 @@
//! ```
use axum::{
body::{self, Body, BoxBody},
body::{self, Body},
extract::State,
handler::Handler,
http::{Request, Response, StatusCode},
@@ -234,7 +234,7 @@ where
mut handler: impl FnMut(server_fn::ServerFnTraitObj<()>) -> H,
) -> Self
where
H: Handler<T, S, Body>,
H: Handler<T, S>,
T: 'static,
S: Clone + Send + Sync + 'static,
{
@@ -468,10 +468,10 @@ pub async fn render_handler(
.await
}
fn report_err<E: std::fmt::Display>(e: E) -> Response<BoxBody> {
fn report_err<E: std::fmt::Display>(e: E) -> Response<axum::body::Body> {
Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(body::boxed(format!("Error: {}", e)))
.body(body::Body::new(format!("Error: {}", e)))
.unwrap()
}
+5 -7
View File
@@ -12,15 +12,13 @@
#[cfg(feature = "axum")]
pub mod axum_adapter;
// #[cfg(feature = "salvo")]
// pub mod salvo_adapter;
#[cfg(feature = "warp")]
pub mod warp_adapter;
use http::StatusCode;
use server_fn::{Encoding, Payload};
use std::sync::{Arc, RwLock};
type MyBody = axum::body::Body;
use crate::{
layer::{BoxedService, Service},
prelude::{DioxusServerContext, ProvideServerContext},
@@ -68,11 +66,11 @@ impl ServerFnHandler {
impl Service for ServerFnHandler {
fn run(
&mut self,
req: http::Request<hyper::body::Body>,
req: http::Request<MyBody>,
) -> std::pin::Pin<
Box<
dyn std::future::Future<
Output = Result<http::Response<hyper::body::Body>, server_fn::ServerFnError>,
Output = Result<http::Response<MyBody>, server_fn::ServerFnError>,
> + Send,
>,
> {
@@ -83,7 +81,7 @@ impl Service for ServerFnHandler {
Box::pin(async move {
let query = req.uri().query().unwrap_or_default().as_bytes().to_vec();
let (parts, body) = req.into_parts();
let body = hyper::body::to_bytes(body).await?.to_vec();
let body = axum::body::to_bytes(body, usize::MAX).await?.to_vec();
let headers = &parts.headers;
let accept_header = headers.get("Accept").cloned();
let parts = Arc::new(RwLock::new(parts));
@@ -1,581 +0,0 @@
//! Dioxus utilities for the [Salvo](https://salvo.rs) server framework.
//!
//! # Example
//! ```rust
//! #![allow(non_snake_case)]
//! use dioxus_lib::prelude::*;
//! use dioxus_fullstack::prelude::*;
//!
//! fn main() {
//! #[cfg(feature = "web")]
//! dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true));
//! #[cfg(feature = "server")]
//! {
//! use salvo::prelude::*;
//! tokio::runtime::Runtime::new()
//! .unwrap()
//! .block_on(async move {
//! let router =
//! Router::new().serve_dioxus_application("", ServerConfig::new(app, ()));
//! Server::new(TcpListener::bind("127.0.0.1:8080"))
//! .serve(router)
//! .await;
//! });
//! }
//! }
//!
//! fn app() -> Element {
//! let text = use_signal(|| "...".to_string());
//!
//! rsx! {
//! button {
//! onclick: move |_| {
//! to_owned![text];
//! 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())
//! }
//! ```
use dioxus_lib::prelude::*;
use http_body_util::{BodyExt, Limited};
use hyper::body::Body as HyperBody;
use hyper::StatusCode;
use salvo::{
async_trait, handler,
http::{
cookie::{Cookie, CookieJar},
ParseError,
},
serve_static::{StaticDir, StaticFile},
Depot, Error as SalvoError, FlowCtrl, Handler, Request, Response, Router,
};
use server_fn::{Encoding, ServerFunctionRegistry};
use std::error::Error;
use std::sync::Arc;
use std::sync::RwLock;
use crate::{
layer::Service, prelude::*, render::SSRState, serve_config::ServeConfig,
server_fn::DioxusServerFnRegistry, server_fn_service,
};
type HyperRequest = hyper::Request<hyper::Body>;
type HyperResponse = hyper::Response<HyperBody>;
/// A extension trait with utilities for integrating Dioxus with your Salvo router.
pub trait DioxusRouterExt {
/// Registers server functions with a custom handler function. This allows you to pass custom context to your server functions by generating a [`DioxusServerContext`] from the request.
///
/// # Example
/// ```rust
/// use salvo::prelude::*;
/// use std::{net::TcpListener, sync::Arc};
/// use dioxus_fullstack::prelude::*;
///
/// struct ServerFunctionHandler {
/// server_fn: server_fn::ServerFnTraitObj<()>,
/// }
///
/// #[handler]
/// impl ServerFunctionHandler {
/// async fn handle(
/// &self,
/// req: &mut Request,
/// depot: &mut Depot,
/// res: &mut Response,
/// flow: &mut FlowCtrl,
/// ) {
/// // Add the headers to server context
/// ServerFnHandler::new((req.headers().clone(),), self.server_fn.clone())
/// .handle(req, depot, res, flow)
/// .await
/// }
/// }
///
/// #[tokio::main]
/// async fn main() {
/// let router = Router::new()
/// .register_server_fns_with_handler("", |func| {
/// ServerFnHandler::new(DioxusServerContext::default(), func)
/// });
/// Server::new(TcpListener::bind("127.0.0.1:8080"))
/// .serve(router)
/// .await;
/// }
/// ```
fn register_server_fns_with_handler<H>(
self,
server_fn_route: &'static str,
handler: impl Fn(server_fn::ServerFnTraitObj<()>) -> H,
) -> Self
where
H: Handler + 'static;
/// Registers server functions with the default handler. This handler function will pass an empty [`DioxusServerContext`] to your server functions.
///
/// # Example
/// ```rust
/// use salvo::prelude::*;
/// use std::{net::TcpListener, sync::Arc};
/// use dioxus_fullstack::prelude::*;
///
/// #[tokio::main]
/// async fn main() {
/// let router = Router::new()
/// .register_server_fns("");
/// Server::new(TcpListener::bind("127.0.0.1:8080"))
/// .serve(router)
/// .await;
/// }
///
/// ```
fn register_server_fns(self, server_fn_route: &'static str) -> Self;
/// Register the web RSX hot reloading endpoint. This will enable hot reloading for your application in debug mode when you call [`dioxus_hot_reload::hot_reload_init`].
///
/// # Example
/// ```rust
/// use salvo::prelude::*;
/// use std::{net::TcpListener, sync::Arc};
/// use dioxus_fullstack::prelude::*;
///
/// #[tokio::main]
/// async fn main() {
/// let router = Router::new()
/// .connect_hot_reload();
/// Server::new(TcpListener::bind("127.0.0.1:8080"))
/// .serve(router)
/// .await;
/// }
fn connect_hot_reload(self) -> Self;
/// Serves the static WASM for your Dioxus application (except the generated index.html).
///
/// # Example
/// ```rust
/// use salvo::prelude::*;
/// use std::{net::TcpListener, sync::Arc};
/// use dioxus_fullstack::prelude::*;
///
/// #[tokio::main]
/// async fn main() {
/// let router = Router::new()
/// .server_static_assets("/dist");
/// Server::new(TcpListener::bind("127.0.0.1:8080"))
/// .serve(router)
/// .await;
/// }
/// ```
fn serve_static_assets(self, assets_path: impl Into<std::path::PathBuf>) -> Self;
/// 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 intigrate with hot reloading.
///
/// # Example
/// ```rust
/// #![allow(non_snake_case)]
/// use dioxus_lib::prelude::*;
/// use dioxus_fullstack::prelude::*;
/// use salvo::prelude::*;
/// use std::{net::TcpListener, sync::Arc};
///
/// #[tokio::main]
/// async fn main() {
/// let router = Router::new().serve_dioxus_application("", ServerConfig::new(app, ()));
/// Server::new(TcpListener::bind("127.0.0.1:8080"))
/// .serve(router)
/// .await;
/// }
///
/// fn app() -> Element {unimplemented!() }
/// ```
fn serve_dioxus_application(
self,
server_fn_path: &'static str,
cfg: impl Into<ServeConfig>,
virtual_dom_factory: impl Fn() -> VirtualDom + Send + Sync + 'static,
) -> Self;
}
impl DioxusRouterExt for Router {
fn register_server_fns_with_handler<H>(
self,
server_fn_route: &'static str,
mut handler: impl FnMut(server_fn::ServerFnTraitObj<()>) -> H,
) -> Self
where
H: Handler + 'static,
{
let mut router = self;
for server_fn_path in DioxusServerFnRegistry::paths_registered() {
let func = DioxusServerFnRegistry::get(server_fn_path).unwrap();
let full_route = format!("{server_fn_route}/{server_fn_path}");
match func.encoding() {
Encoding::Url | Encoding::Cbor => {
router = router.push(Router::with_path(&full_route).post(handler(func)));
}
Encoding::GetJSON | Encoding::GetCBOR => {
router = router.push(Router::with_path(&full_route).get(handler(func)));
}
}
}
router
}
fn register_server_fns(self, server_fn_route: &'static str) -> Self {
self.register_server_fns_with_handler(server_fn_route, |func| ServerFnHandler {
server_context: DioxusServerContext::default(),
function: func,
})
}
fn serve_static_assets(mut self, assets_path: impl Into<std::path::PathBuf>) -> Self {
let assets_path = assets_path.into();
// Serve all files in dist folder except index.html
let dir = std::fs::read_dir(&assets_path).unwrap_or_else(|e| {
panic!(
"Couldn't read assets directory at {:?}: {}",
&assets_path, e
)
});
for entry in dir.flatten() {
let path = entry.path();
if path.ends_with("index.html") {
continue;
}
let route = path
.strip_prefix(&assets_path)
.unwrap()
.iter()
.map(|segment| {
segment.to_str().unwrap_or_else(|| {
panic!("Failed to convert path segment {:?} to string", segment)
})
})
.collect::<Vec<_>>()
.join("/");
if path.is_file() {
let route = format!("/{}", route);
let serve_dir = StaticFile::new(path.clone());
self = self.push(Router::with_path(route).get(serve_dir))
} else {
let route = format!("/{}/<**path>", route);
let serve_dir = StaticDir::new([path.clone()]);
self = self.push(Router::with_path(route).get(serve_dir))
}
}
self
}
fn serve_dioxus_application(
self,
server_fn_path: &'static str,
cfg: impl Into<ServeConfig>,
virtual_dom_factory: impl Fn() -> VirtualDom + Send + Sync + 'static,
) -> Self {
let cfg = cfg.into();
self.serve_static_assets(cfg.assets_path.clone())
.connect_hot_reload()
.register_server_fns(server_fn_path)
.push(Router::with_path("/<**any_path>").get(SSRHandler {
config: cfg,
virtual_dom: virtual_dom_factory,
}))
}
fn connect_hot_reload(self) -> Self {
let mut _dioxus_router = Router::with_path("_dioxus");
_dioxus_router =
_dioxus_router.push(Router::with_path("hot_reload").handle(HotReloadHandler));
#[cfg(all(debug_assertions, feature = "hot-reload"))]
{
_dioxus_router = _dioxus_router.push(Router::with_path("disconnect").handle(ignore_ws));
}
self.push(_dioxus_router)
}
}
/// Extracts the parts of a request that are needed for server functions. This will take parts of the request and replace them with empty values.
pub fn extract_parts(req: &mut Request) -> http::request::Parts {
let mut parts = http::request::Request::new(()).into_parts().0;
parts.method = std::mem::take(req.method_mut());
parts.uri = std::mem::take(req.uri_mut());
parts.version = req.version();
parts.headers = std::mem::take(req.headers_mut());
parts.extensions = std::mem::take(req.extensions_mut());
parts
}
fn apply_request_parts_to_response(
headers: hyper::header::HeaderMap,
response: &mut salvo::prelude::Response,
) {
let mut_headers = response.headers_mut();
for (key, value) in headers.iter() {
mut_headers.insert(key, value.clone());
}
}
#[inline]
async fn convert_request(req: &mut Request) -> Result<HyperRequest, SalvoError> {
let forward_url: hyper::Uri = TryFrom::try_from(req.uri()).map_err(SalvoError::other)?;
let mut build = hyper::Request::builder()
.method(req.method())
.uri(&forward_url);
for (key, value) in req.headers() {
build = build.header(key, value);
}
static SECURE_MAX_SIZE: usize = 64 * 1024;
let body = Limited::new(req.take_body(), SECURE_MAX_SIZE)
.collect()
.await
.map_err(ParseError::other)?
.to_bytes();
build.body(body.into()).map_err(SalvoError::other)
}
#[inline]
async fn convert_response(response: HyperResponse, res: &mut Response) {
let (parts, body) = response.into_parts();
let http::response::Parts {
version,
headers,
status,
..
} = parts;
res.status_code = Some(status);
res.version = version;
res.cookies = CookieJar::new();
for cookie in headers.get_all(http::header::SET_COOKIE).iter() {
if let Some(cookie) = cookie
.to_str()
.ok()
.and_then(|s| Cookie::parse(s.to_string()).ok())
{
res.cookies.add_original(cookie);
}
}
res.headers = headers;
res.version = version;
if let Ok(bytes) = hyper::body::to_bytes(body).await {
res.body = bytes.into()
}
}
/// A handler that renders a Dioxus application to HTML using server-side rendering.
pub struct SSRHandler {
config: ServeConfig,
virtual_dom: Box<dyn Fn() -> VirtualDom + Send + Sync>,
}
impl SSRHandler {
/// Creates a new SSR handler with the given configuration.
pub fn new(
config: ServeConfig,
virtual_dom: impl Fn() -> VirtualDom + Send + Sync + 'static,
) -> Self {
Self {
config,
virtual_dom: Box::new(virtual_dom),
}
}
}
#[async_trait]
impl Handler for SSRHandler {
async fn handle(
&self,
req: &mut Request,
depot: &mut Depot,
res: &mut Response,
_flow: &mut FlowCtrl,
) {
// Get the SSR renderer from the depot or create a new one if it doesn't exist
let renderer_pool = if let Some(renderer) = depot.obtain::<SSRState>() {
renderer.clone()
} else {
let renderer = SSRState::new(&self.cfg);
depot.inject(renderer.clone());
renderer
};
let route = req.uri().path().to_string();
let parts: Arc<RwLock<http::request::Parts>> = Arc::new(RwLock::new(extract_parts(req)));
let server_context = DioxusServerContext::new(parts);
match renderer_pool
.render(route, &self.cfg, &server_context)
.await
{
Ok(rendered) => {
let crate::render::RenderResponse { html, freshness } = rendered;
res.write_body(html).unwrap();
let headers = server_context.response_parts().unwrap().headers.clone();
apply_request_parts_to_response(headers, res);
freshness.write(res.headers_mut());
}
Err(err) => {
tracing::error!("Error rendering SSR: {}", err);
res.write_body("Error rendering SSR").unwrap();
}
};
}
}
/// A default handler for server functions. It will deserialize the request body, call the server function, and serialize the response.
pub struct ServerFnHandler {
server_context: DioxusServerContext,
function: server_fn::ServerFnTraitObj<()>,
}
impl ServerFnHandler {
/// Create a new server function handler with the given server context and server function.
pub fn new(
server_context: impl Into<DioxusServerContext>,
function: server_fn::ServerFnTraitObj<()>,
) -> Self {
let server_context = server_context.into();
Self {
server_context,
function,
}
}
}
#[handler]
impl ServerFnHandler {
async fn handle(&self, req: &mut Request, _depot: &mut Depot, res: &mut Response) {
match convert_request(req).await {
Ok(hyper_req) => {
let response =
server_fn_service(self.server_context.clone(), self.function.clone())
.run(hyper_req)
.await
.unwrap();
convert_response(response, res).await;
}
Err(err) => handle_error(err, res),
}
}
}
fn handle_error(error: impl Error + Send + Sync, res: &mut Response) {
let mut resp_err = Response::new();
resp_err.status_code(StatusCode::INTERNAL_SERVER_ERROR);
resp_err.render(format!("Internal Server Error: {}", error));
*res = resp_err;
}
/// A handler for Dioxus web hot reload websocket. This will send the updated static parts of the RSX to the client when they change.
#[cfg(not(all(debug_assertions, feature = "hot-reload")))]
#[derive(Default)]
pub struct HotReloadHandler;
#[cfg(not(all(debug_assertions, feature = "hot-reload")))]
#[handler]
impl HotReloadHandler {
async fn handle(
&self,
_req: &mut Request,
_depot: &mut Depot,
_res: &mut Response,
) -> Result<(), salvo::http::StatusError> {
Err(salvo::http::StatusError::not_found())
}
}
/// A handler for Dioxus web hot reload websocket. This will send the updated static parts of the RSX to the client when they change.
#[cfg(all(debug_assertions, feature = "hot-reload"))]
#[derive(Default)]
pub struct HotReloadHandler;
#[cfg(all(debug_assertions, feature = "hot-reload"))]
#[handler]
impl HotReloadHandler {
async fn handle(
&self,
req: &mut Request,
_depot: &mut Depot,
res: &mut Response,
) -> Result<(), salvo::http::StatusError> {
use salvo::websocket::Message;
use salvo::websocket::WebSocketUpgrade;
let state = crate::hot_reload::spawn_hot_reload().await;
WebSocketUpgrade::new()
.upgrade(req, res, move |mut websocket| async move {
use futures_util::StreamExt;
println!("🔥 Hot Reload WebSocket connected");
{
// update any rsx calls that changed before the websocket connected.
{
println!("🔮 Finding updates since last compile...");
let templates_read = state.templates.read().await;
for template in &*templates_read {
if websocket
.send(Message::text(serde_json::to_string(&template).unwrap()))
.await
.is_err()
{
return;
}
}
}
println!("finished");
}
let mut rx = tokio_stream::wrappers::WatchStream::from_changes(
state.message_receiver.clone(),
);
while let Some(change) = rx.next().await {
if let Some(template) = change {
let template = { serde_json::to_string(&template).unwrap() };
if websocket.send(Message::text(template)).await.is_err() {
break;
};
}
}
})
.await
}
}
#[cfg(all(debug_assertions, feature = "hot-reload"))]
#[handler]
async fn ignore_ws(req: &mut Request, res: &mut Response) -> Result<(), salvo::http::StatusError> {
use salvo::websocket::WebSocketUpgrade;
WebSocketUpgrade::new()
.upgrade(req, res, |mut ws| async move {
let _ = ws.send(salvo::websocket::Message::text("connected")).await;
while let Some(msg) = ws.recv().await {
if msg.is_err() {
return;
};
}
})
.await
}
@@ -1,417 +0,0 @@
//! Dioxus utilities for the [Warp](https://docs.rs/warp/latest/warp/index.html) server framework.
//!
//! # Example
//! ```rust
//! #![allow(non_snake_case)]
//! use dioxus_lib::prelude::*;
//! use dioxus_fullstack::prelude::*;
//!
//! fn main() {
//! #[cfg(feature = "web")]
//! dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true));
//! #[cfg(feature = "server")]
//! {
//! tokio::runtime::Runtime::new()
//! .unwrap()
//! .block_on(async move {
//! let routes = serve_dioxus_application("", ServerConfig::new(app, ()));
//! warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
//! });
//! }
//! }
//!
//! fn app() -> Element {
//! let text = use_signal(|| "...".to_string());
//!
//! rsx! {
//! button {
//! onclick: move |_| {
//! to_owned![text];
//! 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())
//! }
//!
//! ```
use crate::layer::Service;
use crate::{
prelude::*, render::SSRState, serve_config::ServeConfig, server_fn::DioxusServerFnRegistry,
};
use crate::server_fn_service;
use dioxus_lib::prelude::VirtualDom;
use server_fn::{Encoding, Payload, ServerFunctionRegistry};
use std::error::Error;
use std::sync::Arc;
use std::sync::RwLock;
use tokio::task::spawn_blocking;
use warp::path::FullPath;
use warp::Rejection;
use warp::{
filters::BoxedFilter,
http::{Response, StatusCode},
hyper::body::Bytes,
path, Filter, Reply,
};
/// Registers server functions with a custom handler function. This allows you to pass custom context to your server functions by generating a [`DioxusServerContext`] from the request.
///
/// # Example
/// ```rust
/// use warp::{body, header, hyper::HeaderMap, path, post, Filter};
///
/// #[tokio::main]
/// async fn main() {
/// let routes = register_server_fns_with_handler(server_fn_route, |full_route, func| {
/// path(full_route)
/// .and(warp::post().or(warp::get()).unify())
/// .and(request_parts())
/// .and(warp::body::bytes())
/// .and_then(move |parts, bytes: bytes::Bytes| {
/// let mut service = server_fn_service(DioxusServerContext::default(), func.clone());
/// async move {
/// let req = warp::hyper::Request::from_parts(parts, bytes.into());
/// service.run(req).await.map_err(|err| {
/// tracing::error!("Server function error: {}", err);
/// warp::reject::reject()
/// })
/// }
/// })
/// })
/// warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
/// }
/// ```
pub fn register_server_fns_with_handler<H, F, R>(
server_fn_route: &'static str,
mut handler: H,
) -> BoxedFilter<(R,)>
where
H: FnMut(String, server_fn::ServerFnTraitObj<()>) -> F,
F: Filter<Extract = (R,), Error = warp::Rejection> + Send + Sync + 'static,
F::Extract: Send,
R: Reply + 'static,
{
let mut filter: Option<BoxedFilter<F::Extract>> = None;
for server_fn_path in DioxusServerFnRegistry::paths_registered() {
let func = DioxusServerFnRegistry::get(server_fn_path).unwrap();
let full_route = format!("{server_fn_route}/{server_fn_path}")
.trim_start_matches('/')
.to_string();
let route = handler(full_route, func).boxed();
if let Some(boxed_filter) = filter.take() {
filter = Some(boxed_filter.or(route).unify().boxed());
} else {
filter = Some(route);
}
}
filter.expect("No server functions found")
}
/// Registers server functions with the default handler. This handler function will pass an empty [`DioxusServerContext`] to your server functions.
///
/// # Example
/// ```rust
/// use dioxus_fullstack::prelude::*;
///
/// #[tokio::main]
/// async fn main() {
/// let routes = register_server_fns("");
/// warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
/// }
/// ```
pub fn register_server_fns(server_fn_route: &'static str) -> BoxedFilter<(impl Reply,)> {
register_server_fns_with_handler(server_fn_route, |full_route, func| {
path(full_route)
.and(warp::post().or(warp::get()).unify())
.and(request_parts())
.and(warp::body::bytes())
.and_then(move |parts, bytes: bytes::Bytes| {
let mut service = server_fn_service(DioxusServerContext::default(), func.clone());
async move {
let req = warp::hyper::Request::from_parts(parts, bytes.into());
service.run(req).await.map_err(|err| {
tracing::error!("Server function error: {}", err);
struct WarpServerFnError(String);
impl std::fmt::Debug for WarpServerFnError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl warp::reject::Reject for WarpServerFnError {}
warp::reject::custom(WarpServerFnError(err.to_string()))
})
}
})
})
}
/// 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 intigrate with hot reloading.
///
/// # Example
/// ```rust
/// #![allow(non_snake_case)]
/// use dioxus_lib::prelude::*;
/// use dioxus_fullstack::prelude::*;
///
/// #[tokio::main]
/// async fn main() {
/// let routes = serve_dioxus_application("", ServerConfig::new(app, ()));
/// warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
/// }
///
/// fn app() -> Element {
/// None
/// }
/// ```
pub fn serve_dioxus_application(
server_fn_route: &'static str,
cfg: impl Into<ServeConfig>,
virtual_dom_factory: impl Fn() -> VirtualDom + Send + Sync + 'static,
) -> BoxedFilter<(impl Reply,)> {
let cfg = cfg.into();
// Serve the dist folder and the index.html file
let serve_dir = warp::fs::dir(cfg.assets_path.clone());
let virtual_dom_factory =
Arc::new(virtual_dom_factory) as Arc<dyn Fn() -> VirtualDom + Send + Sync + 'static>;
connect_hot_reload()
// First register the server functions
.or(register_server_fns(server_fn_route))
// Then the index route
.or(path::end().and(render_ssr(cfg.clone(), {
let virtual_dom_factory = virtual_dom_factory.clone();
move || virtual_dom_factory()
})))
// Then the static assets
.or(serve_dir)
// Then all other routes
.or(render_ssr(cfg, move || virtual_dom_factory()))
.boxed()
}
/// Server render the application.
pub fn render_ssr(
cfg: ServeConfig,
virtual_dom_factory: impl Fn() -> VirtualDom + Send + Sync + 'static,
) -> impl Filter<Extract = (impl Reply,), Error = warp::Rejection> + Clone {
warp::get()
.and(request_parts())
.and(with_ssr_state(&cfg, virtual_dom_factory))
.then(
move |parts: http::request::Parts,
(renderer, virtual_dom_factory): (
SSRState,
Arc<dyn Fn() -> VirtualDom + Send + Sync + 'static>,
)| {
let route = parts.uri.path().to_string();
let parts = Arc::new(RwLock::new(parts));
let cfg = cfg.clone();
async move {
let server_context = DioxusServerContext::new(parts);
match renderer
.render(route, &cfg, move || virtual_dom_factory(), &server_context)
.await
{
Ok(rendered) => {
let crate::render::RenderResponse { html, freshness } = rendered;
let mut res = Response::builder()
.header("Content-Type", "text/html")
.body(html)
.unwrap();
let headers_mut = res.headers_mut();
let headers = server_context.response_parts().unwrap().headers.clone();
for (key, value) in headers.iter() {
headers_mut.insert(key, value.clone());
}
freshness.write(headers_mut);
res
}
Err(err) => {
tracing::error!("Failed to render ssr: {}", err);
Response::builder()
.status(500)
.body("Failed to render ssr".into())
.unwrap()
}
}
}
},
)
}
/// An extractor for the request parts (used in [DioxusServerContext]). This will extract the method, uri, query, and headers from the request.
pub fn request_parts(
) -> impl Filter<Extract = (http::request::Parts,), Error = warp::reject::Rejection> + Clone {
warp::method()
.and(warp::filters::path::full())
.and(
warp::filters::query::raw()
.or(warp::any().map(String::new))
.unify(),
)
.and(warp::header::headers_cloned())
.and_then(move |method, path: FullPath, query, headers| async move {
http::uri::Builder::new()
.path_and_query(format!("{}?{}", path.as_str(), query))
.build()
.map_err(|err| {
warp::reject::custom(FailedToReadBody(format!("Failed to build uri: {}", err)))
})
.map(|uri| {
let mut req = http::Request::builder()
.method(method)
.uri(uri)
.body(())
.unwrap();
req.headers_mut().extend(headers);
req.into_parts().0
})
})
}
fn with_ssr_state(
cfg: &ServeConfig,
virtual_dom_factory: impl Fn() -> VirtualDom + Send + Sync + 'static,
) -> impl Filter<
Extract = ((
SSRState,
Arc<dyn Fn() -> VirtualDom + Send + Sync + 'static>,
),),
Error = std::convert::Infallible,
> + Clone {
let renderer = SSRState::new(cfg);
let virtual_dom_factory =
Arc::new(virtual_dom_factory) as Arc<dyn Fn() -> VirtualDom + Send + Sync + 'static>;
warp::any().map(move || (renderer.clone(), virtual_dom_factory.clone()))
}
#[derive(Debug)]
struct FailedToReadBody(String);
impl warp::reject::Reject for FailedToReadBody {}
#[derive(Debug)]
struct RecieveFailed(String);
impl warp::reject::Reject for RecieveFailed {}
/// Register the web RSX hot reloading endpoint. This will enable hot reloading for your application in debug mode when you call [`dioxus_hot_reload::hot_reload_init`].
///
/// # Example
/// ```rust
/// #![allow(non_snake_case)]
/// use dioxus_fullstack::prelude::*;
///
/// #[tokio::main]
/// async fn main() {
/// let routes = connect_hot_reload();
/// warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
/// }
/// ```
pub fn connect_hot_reload() -> impl Filter<Extract = (impl Reply,), Error = warp::Rejection> + Clone
{
#[cfg(not(all(debug_assertions, feature = "hot-reload")))]
{
warp::path!("_dioxus" / "hot_reload")
.map(warp::reply)
.map(|reply| warp::reply::with_status(reply, warp::http::StatusCode::NOT_FOUND))
}
#[cfg(all(debug_assertions, feature = "hot-reload"))]
{
use crate::hot_reload::HotReloadState;
use futures_util::sink::SinkExt;
use futures_util::StreamExt;
use warp::ws::Message;
let hot_reload = warp::path!("_dioxus" / "hot_reload")
.and(warp::any().then(crate::hot_reload::spawn_hot_reload))
.and(warp::ws())
.map(move |state: &'static HotReloadState, ws: warp::ws::Ws| {
// #[cfg(all(debug_assertions, feature = "hot-reload"))]
ws.on_upgrade(move |mut websocket| {
async move {
println!("🔥 Hot Reload WebSocket connected");
{
// update any rsx calls that changed before the websocket connected.
{
println!("🔮 Finding updates since last compile...");
let templates_read = state.templates.read().await;
for template in &*templates_read {
if websocket
.send(Message::text(
serde_json::to_string(&template).unwrap(),
))
.await
.is_err()
{
return;
}
}
}
println!("finished");
}
let mut rx = tokio_stream::wrappers::WatchStream::from_changes(
state.message_receiver.clone(),
);
while let Some(change) = rx.next().await {
if let Some(template) = change {
let template = { serde_json::to_string(&template).unwrap() };
if websocket.send(Message::text(template)).await.is_err() {
break;
};
}
}
}
})
});
let disconnect =
warp::path!("_dioxus" / "disconnect")
.and(warp::ws())
.map(move |ws: warp::ws::Ws| {
println!("disconnect");
#[cfg(all(debug_assertions, feature = "hot-reload",))]
ws.on_upgrade(move |mut websocket| async move {
struct DisconnectOnDrop(Option<warp::ws::WebSocket>);
impl Drop for DisconnectOnDrop {
fn drop(&mut self) {
std::mem::drop(self.0.take().unwrap().close());
}
}
let _ = websocket.send(Message::text("connected")).await;
let mut ws = DisconnectOnDrop(Some(websocket));
loop {
if ws.0.as_mut().unwrap().next().await.is_none() {
break;
}
}
})
});
disconnect.or(hot_reload)
}
}
+12 -55
View File
@@ -53,7 +53,7 @@ impl Config {
/// Set the address to serve the app on.
#[cfg(feature = "server")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "server")))]
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
pub fn addr(self, addr: impl Into<std::net::SocketAddr>) -> Self {
let addr = addr.into();
Self { addr, ..self }
@@ -61,7 +61,7 @@ impl Config {
/// Set the route to the server functions.
#[cfg(feature = "server")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "server")))]
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
pub fn server_fn_route(self, server_fn_route: &'static str) -> Self {
Self {
server_fn_route,
@@ -71,7 +71,7 @@ impl Config {
/// Set the incremental renderer config.
#[cfg(feature = "server")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "server")))]
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
pub fn incremental(self, cfg: IncrementalRendererConfig) -> Self {
Self {
server_cfg: self.server_cfg.incremental(cfg),
@@ -81,21 +81,21 @@ impl Config {
/// Set the server config.
#[cfg(feature = "server")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "server")))]
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
pub fn server_cfg(self, server_cfg: ServeConfigBuilder) -> Self {
Self { server_cfg, ..self }
}
/// Set the web config.
#[cfg(feature = "web")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "web")))]
#[cfg_attr(docsrs, doc(cfg(feature = "web")))]
pub fn web_cfg(self, web_cfg: dioxus_web::Config) -> Self {
Self { web_cfg, ..self }
}
/// Set the desktop config.
#[cfg(feature = "desktop")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "desktop")))]
#[cfg_attr(docsrs, doc(cfg(feature = "desktop")))]
pub fn desktop_cfg(self, desktop_cfg: dioxus_desktop::Config) -> Self {
Self {
desktop_cfg,
@@ -105,13 +105,13 @@ impl Config {
/// Set the mobile config.
#[cfg(feature = "mobile")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "mobile")))]
#[cfg_attr(docsrs, doc(cfg(feature = "mobile")))]
pub fn mobile_cfg(self, mobile_cfg: dioxus_mobile::Config) -> Self {
Self { mobile_cfg, ..self }
}
#[cfg(feature = "server")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "server")))]
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
/// Launch a server application
pub async fn launch_server(
self,
@@ -121,7 +121,8 @@ impl Config {
println!("Listening on {}", addr);
let cfg = self.server_cfg.build();
let server_fn_route = self.server_fn_route;
#[cfg(all(feature = "axum", not(feature = "warp"), not(feature = "salvo")))]
#[cfg(feature = "axum")]
{
use crate::adapters::axum_adapter::{render_handler, DioxusRouterExt};
use axum::routing::get;
@@ -144,52 +145,8 @@ impl Config {
.layer(tower_http::compression::CompressionLayer::new().gzip(true)),
)
.into_make_service();
axum::Server::bind(&addr).serve(router).await.unwrap();
}
#[cfg(all(feature = "warp", not(feature = "axum"), not(feature = "salvo")))]
{
use warp::Filter;
// First register the server functions
let router = register_server_fns(server_fn_route);
#[cfg(not(any(feature = "desktop", feature = "mobile")))]
let router = {
// Serve the dist folder and the index.html file
let serve_dir = warp::fs::dir(cfg.assets_path);
let build_virtual_dom = Arc::new(build_virtual_dom);
router
.or(connect_hot_reload())
// Then the index route
.or(warp::path::end().and(render_ssr(cfg.clone(), {
let build_virtual_dom = build_virtual_dom.clone();
move || build_virtual_dom()
})))
// Then the static assets
.or(serve_dir)
// Then all other routes
.or(render_ssr(cfg, move || build_virtual_dom()))
};
warp::serve(router.boxed().with(warp::filters::compression::gzip()))
.run(addr)
.await;
}
#[cfg(all(feature = "salvo", not(feature = "axum"), not(feature = "warp")))]
{
use crate::adapters::salvo_adapter::{DioxusRouterExt, SSRHandler};
use salvo::conn::Listener;
let router = salvo::Router::new().register_server_fns(server_fn_route);
#[cfg(not(any(feature = "desktop", feature = "mobile")))]
let router = router
.serve_static_assets(cfg.assets_path)
.connect_hot_reload()
.push(salvo::Router::with_path("/<**any_path>").get(SSRHandler::new(cfg)));
let router = router.hoop(
salvo::compression::Compression::new()
.enable_gzip(salvo::prelude::CompressionLevel::Default),
);
salvo::Server::new(salvo::conn::tcp::TcpListener::new(addr).bind().await)
.serve(router)
.await;
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, router).await.unwrap();
}
}
}
+13 -13
View File
@@ -19,35 +19,35 @@ where
}
}
type MyBody = axum::body::Body;
/// A service is a function that takes a request and returns an async response
pub trait Service {
/// Run the service and produce a future that resolves to a response
fn run(
&mut self,
req: http::Request<hyper::body::Body>,
req: http::Request<MyBody>,
) -> Pin<
Box<
dyn std::future::Future<
Output = Result<Response<hyper::body::Body>, server_fn::ServerFnError>,
> + Send,
dyn std::future::Future<Output = Result<Response<MyBody>, server_fn::ServerFnError>>
+ Send,
>,
>;
}
impl<S> Service for S
where
S: tower::Service<http::Request<hyper::body::Body>, Response = Response<hyper::body::Body>>,
S: tower::Service<http::Request<MyBody>, Response = Response<MyBody>>,
S::Future: Send + 'static,
S::Error: Into<server_fn::ServerFnError>,
{
fn run(
&mut self,
req: http::Request<hyper::body::Body>,
req: http::Request<MyBody>,
) -> Pin<
Box<
dyn std::future::Future<
Output = Result<Response<hyper::body::Body>, server_fn::ServerFnError>,
> + Send,
dyn std::future::Future<Output = Result<Response<MyBody>, server_fn::ServerFnError>>
+ Send,
>,
> {
let fut = self.call(req).instrument(tracing::trace_span!(
@@ -62,13 +62,13 @@ where
/// A boxed service is a type-erased service that can be used without knowing the underlying type
pub struct BoxedService(pub Box<dyn Service + Send>);
impl tower::Service<http::Request<hyper::body::Body>> for BoxedService {
type Response = http::Response<hyper::body::Body>;
impl tower::Service<http::Request<MyBody>> for BoxedService {
type Response = http::Response<MyBody>;
type Error = server_fn::ServerFnError;
type Future = Pin<
Box<
dyn std::future::Future<
Output = Result<http::Response<hyper::body::Body>, server_fn::ServerFnError>,
Output = Result<http::Response<MyBody>, server_fn::ServerFnError>,
> + Send,
>,
>;
@@ -80,7 +80,7 @@ impl tower::Service<http::Request<hyper::body::Body>> for BoxedService {
Ok(()).into()
}
fn call(&mut self, req: Request<hyper::body::Body>) -> Self::Future {
fn call(&mut self, req: Request<MyBody>) -> Self::Future {
self.0.run(req)
}
}
+25 -45
View File
@@ -11,21 +11,17 @@ mod html_storage;
#[cfg(feature = "server")]
mod adapters;
// Splitting up the glob export lets us document features required for each adapter
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "axum")))]
#[cfg_attr(docsrs, doc(cfg(feature = "axum")))]
#[cfg(feature = "axum")]
pub use adapters::axum_adapter;
// TODO: Compilation seems to be broken with the salvo feature enabled. Fix and add more features to checks in CI
// #[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "salvo")))]
// #[cfg(feature = "salvo")]
// pub use adapters::salvo_adapter;
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "warp")))]
#[cfg(feature = "warp")]
pub use adapters::warp_adapter;
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "server")))]
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
#[cfg(feature = "server")]
pub use adapters::{server_fn_service, ServerFnHandler};
mod config;
mod hooks;
#[cfg(all(debug_assertions, feature = "hot-reload", feature = "server"))]
mod hot_reload;
pub mod launch;
@@ -43,15 +39,11 @@ mod server_fn;
/// A prelude of commonly used items in dioxus-fullstack.
pub mod prelude {
#[cfg(feature = "axum")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "axum")))]
#[cfg_attr(docsrs, doc(cfg(feature = "axum")))]
pub use crate::adapters::axum_adapter::*;
// #[cfg(feature = "salvo")]
// #[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "salvo")))]
// pub use crate::adapters::salvo_adapter::*;
#[cfg(feature = "warp")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "warp")))]
pub use crate::adapters::warp_adapter::*;
use crate::hooks;
#[cfg(not(feature = "server"))]
#[cfg_attr(
any(docsrs, feature = "nightly-doc"),
@@ -59,8 +51,9 @@ pub mod prelude {
)]
pub use crate::html_storage::deserialize::get_root_props_from_document;
#[cfg(feature = "server")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "server")))]
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
pub use crate::layer::{Layer, Service};
#[cfg(all(feature = "server", feature = "router"))]
#[cfg_attr(
any(docsrs, feature = "nightly-doc"),
@@ -68,13 +61,15 @@ pub mod prelude {
)]
pub use crate::render::pre_cache_static_routes_with_props;
#[cfg(feature = "server")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "server")))]
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
pub use crate::render::SSRState;
#[cfg(feature = "router")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "router")))]
#[cfg_attr(docsrs, doc(cfg(feature = "router")))]
pub use crate::router::FullstackRouterConfig;
#[cfg(feature = "server")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "server")))]
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
pub use crate::serve_config::{ServeConfig, ServeConfigBuilder};
#[cfg(all(feature = "server", feature = "axum"))]
#[cfg_attr(
@@ -83,44 +78,29 @@ pub mod prelude {
)]
pub use crate::server_context::Axum;
#[cfg(feature = "server")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "server")))]
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
pub use crate::server_context::{
extract, server_context, DioxusServerContext, FromServerContext, ProvideServerContext,
};
pub use crate::server_fn::DioxusServerFn;
#[cfg(feature = "server")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "server")))]
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
pub use crate::server_fn::{ServerFnMiddleware, ServerFnTraitObj, ServerFunction};
pub use dioxus_server_macro::*;
#[cfg(feature = "server")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "server")))]
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
pub use dioxus_ssr::incremental::IncrementalRendererConfig;
pub use server_fn::{self, ServerFn as _, ServerFnError};
pub use hooks::{server_cached::server_cached, server_future::use_server_future};
}
// Warn users about overlapping features
#[cfg(all(
feature = "server",
feature = "web",
not(doc),
not(feature = "nightly-doc")
))]
compile_error!("The `ssr` feature (enabled by `warp`, `axum`, or `salvo`) and `web` feature are overlapping. Please choose one or the other.");
// // Warn users about overlapping features
// #[cfg(all(feature = "server", feature = "web", not(doc)))]
// compile_error!("The `ssr` feature (enabled by `warp`, `axum`, or `salvo`) and `web` feature are overlapping. Please choose one or the other.");
#[cfg(all(
feature = "server",
feature = "desktop",
not(doc),
not(feature = "nightly-doc")
))]
compile_error!("The `ssr` feature (enabled by `warp`, `axum`, or `salvo`) and `desktop` feature are overlapping. Please choose one or the other.");
// #[cfg(all(feature = "server", feature = "desktop", not(doc)))]
// compile_error!("The `ssr` feature (enabled by `warp`, `axum`, or `salvo`) and `desktop` feature are overlapping. Please choose one or the other.");
#[cfg(all(
feature = "server",
feature = "mobile",
not(doc),
not(feature = "nightly-doc")
))]
compile_error!("The `ssr` feature (enabled by `warp`, `axum`, or `salvo`) and `mobile` feature are overlapping. Please choose one or the other.");
// #[cfg(all(feature = "server", feature = "mobile", not(doc)))]
// compile_error!("The `ssr` feature (enabled by `warp`, `axum`, or `salvo`) and `mobile` feature are overlapping. Please choose one or the other.");
+1 -1
View File
@@ -228,7 +228,7 @@ impl<T: Send + Sync + Clone + 'static> FromServerContext for FromContext<T> {
}
#[cfg(feature = "axum")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "axum")))]
#[cfg_attr(docsrs, doc(cfg(feature = "axum")))]
/// An adapter for axum extractors for the server context
pub struct Axum;
+6 -6
View File
@@ -1,5 +1,5 @@
#[cfg(feature = "server")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "server")))]
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
#[derive(Clone)]
/// A trait object for a function that be called on serializable arguments and returns a serializable result.
pub struct ServerFnTraitObj(server_fn::ServerFnTraitObj<()>);
@@ -41,7 +41,7 @@ impl ServerFnTraitObj {
server_fn::inventory::collect!(ServerFnTraitObj);
#[cfg(feature = "server")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "server")))]
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
/// Middleware for a server function
pub struct ServerFnMiddleware {
/// The prefix of the server function.
@@ -75,7 +75,7 @@ pub(crate) static MIDDLEWARE: once_cell::sync::Lazy<
server_fn::inventory::collect!(ServerFnMiddleware);
#[cfg(feature = "server")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "server")))]
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
/// A server function that can be called on serializable arguments and returns a serializable result.
pub type ServerFunction = server_fn::SerializedFnTraitObj<()>;
@@ -92,7 +92,7 @@ static REGISTERED_SERVER_FUNCTIONS: once_cell::sync::Lazy<
});
#[cfg(feature = "server")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "server")))]
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
/// The registry of all Dioxus server functions.
pub struct DioxusServerFnRegistry;
@@ -160,7 +160,7 @@ impl server_fn::ServerFunctionRegistry<()> for DioxusServerFnRegistry {
}
#[cfg(feature = "server")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "server")))]
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
/// Errors that can occur when registering a server function.
#[derive(thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum ServerRegistrationFnError {
@@ -184,7 +184,7 @@ pub enum ServerRegistrationFnError {
pub trait DioxusServerFn: server_fn::ServerFn<()> {
/// Registers the server function, allowing the client to query it by URL.
#[cfg(feature = "server")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "server")))]
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
fn register_explicit() -> Result<(), server_fn::ServerFnError> {
Self::register_in_explicit::<DioxusServerFnRegistry>()
}