Merge branch 'master' into pr/atty303/1948

This commit is contained in:
Evan Almloff committed 2024-02-19 08:35:31 -06:00
commit c4a4a31eec
78 files changed
+1277 -4465

No files matched your search

+1 -1
View File
@@ -12,7 +12,7 @@ keywords = ["react", "gui", "cli", "dioxus", "wasm"]
clap = { version = "4.2", features = ["derive"], optional = true }
serde = { version = "1.0.136", features = ["derive"] }
serde_json = "1.0.79"
toml = { version = "0.5.8", optional = true }
toml = { workspace = true, optional = true }
cargo_toml = { version = "0.16.0", optional = true }
once_cell = "1.18.0"
tracing = { workspace = true }
+11 -9
View File
@@ -21,7 +21,7 @@ log = "0.4.14"
fern = { version = "0.6.0", features = ["colored"] }
serde = { version = "1.0.136", features = ["derive"] }
serde_json = "1.0.79"
toml = "0.8.8"
toml = {workspace = true}
fs_extra = "1.2.0"
cargo_toml = "0.18.0"
futures-util = { workspace = true }
@@ -32,22 +32,24 @@ tokio = { version = "1.16.1", features = ["fs", "sync", "rt", "macros"] }
atty = "0.2.14"
chrono = "0.4.19"
anyhow = "1"
hyper = "0.14.17"
hyper-rustls = "0.23.2"
hyper = { workspace = true }
hyper-util = "0.1.3"
hyper-rustls = { workspace = true }
indicatif = "0.17.5"
subprocess = "0.2.9"
rayon = "1.8.0"
axum = { version = "0.5.1", features = ["ws", "headers"] }
axum-server = { version = "0.5.1", features = ["tls-rustls"] }
tower-http = { version = "0.2.2", features = ["full"] }
headers = "0.3.7"
axum = { workspace = true, features = ["ws"] }
axum-server = { workspace = true, features = ["tls-rustls"] }
axum-extra = { workspace = true, features = ["typed-header"] }
tower-http = { workspace = true, features = ["full"] }
headers = "0.3.7"
walkdir = "2"
# tools download
dirs = "5.0.1"
reqwest = { version = "0.11", features = [
reqwest = { workspace = true, features = [
"rustls-tls",
"stream",
"trust-dns",
@@ -56,7 +58,7 @@ reqwest = { version = "0.11", features = [
flate2 = "1.0.22"
tar = "0.4.38"
zip = "0.6.2"
tower = "0.4.12"
tower = { workspace = true }
lazy_static = "1.4.0"
# plugin packages
+2 -4
View File
@@ -1,6 +1,4 @@
use crate::assets::AssetConfigDropGuard;
#[cfg(feature = "plugin")]
use crate::plugin::PluginManager;
use crate::server::fullstack;
use dioxus_cli_config::Platform;
@@ -55,7 +53,7 @@ impl Build {
crate_config.set_cargo_args(self.build.cargo_args.clone());
// #[cfg(feature = "plugin")]
// let _ = PluginManager::on_build_start(&crate_config, &platform);
// let _ = crate::plugin::PluginManager::on_build_start(&crate_config, &platform);
let build_result = match platform {
Platform::Web => {
@@ -116,7 +114,7 @@ impl Build {
file.write_all(temp.as_bytes())?;
// #[cfg(feature = "plugin")]
// let _ = PluginManager::on_build_finish(&crate_config, &platform);
// let _ = crate::plugin::PluginManager::on_build_finish(&crate_config, &platform);
Ok(())
}
+3 -3
View File
@@ -5,9 +5,6 @@ use anyhow::Context;
use clap::Parser;
use dioxus_cli::*;
#[cfg(feature = "plugin")]
use dioxus_cli::plugin::PluginManager;
use Commands::*;
fn get_bin(bin: Option<String>) -> Result<PathBuf> {
@@ -92,6 +89,9 @@ async fn main() -> anyhow::Result<()> {
DioxusConfig::default()
});
#[cfg(feature = "plugin")]
use dioxus_cli::plugin::PluginManager;
#[cfg(feature = "plugin")]
PluginManager::init(_dioxus_config.plugin)
.context(error_wrapper("Plugin system initialization failed"))?;
+1 -1
View File
@@ -45,7 +45,7 @@ impl UserData for PluginCommander {
if cmd.is_empty() {
return Ok(());
}
let cmd_name = cmd.get(0).unwrap();
let cmd_name = cmd.first().unwrap();
let mut command = Command::new(cmd_name);
let t = cmd
.iter()
+2 -4
View File
@@ -1,14 +1,12 @@
use crate::server::HotReloadState;
use axum::{
extract::{ws::Message, WebSocketUpgrade},
response::IntoResponse,
Extension, TypedHeader,
Extension,
};
use crate::server::HotReloadState;
pub async fn hot_reload_handler(
ws: WebSocketUpgrade,
_: Option<TypedHeader<headers::UserAgent>>,
Extension(state): Extension<HotReloadState>,
) -> impl IntoResponse {
ws.on_upgrade(|mut socket| async move {
+9 -13
View File
@@ -8,8 +8,8 @@ use crate::{
BuildResult, Result,
};
use axum::{
body::{Full, HttpBody},
extract::{ws::Message, Extension, TypedHeader, WebSocketUpgrade},
body::Body,
extract::{ws::Message, Extension, WebSocketUpgrade},
http::{
self,
header::{HeaderName, HeaderValue},
@@ -287,20 +287,18 @@ async fn setup_router(
let mut response = if file_service_config.dioxus_config.web.watcher.index_on_404
&& response.status() == StatusCode::NOT_FOUND
{
let body = Full::from(
let body = Body::from(
// TODO: Cache/memoize this.
std::fs::read_to_string(file_service_config.out_dir().join("index.html"))
.ok()
.unwrap(),
)
.map_err(|err| match err {})
.boxed();
);
Response::builder()
.status(StatusCode::OK)
.body(body)
.unwrap()
} else {
response.map(|body| body.boxed())
response.into_response()
};
let headers = response.headers_mut();
headers.insert(
@@ -324,7 +322,7 @@ async fn setup_router(
// Route file service
router = router.fallback(get_service(file_service).handle_error(
|error: std::io::Error| async move {
|error: std::convert::Infallible| async move {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Unhandled internal error: {}", error),
@@ -335,7 +333,7 @@ async fn setup_router(
router = if let Some(base_path) = config.dioxus_config.web.app.base_path.clone() {
let base_path = format!("/{}", base_path.trim_matches('/'));
Router::new()
.nest(&base_path, axum::routing::any_service(router))
.route(&base_path, axum::routing::any_service(router))
.fallback(get(move || {
let base_path = base_path.clone();
async move { format!("Outside of the base path: {}", base_path) }
@@ -388,9 +386,8 @@ async fn start_server(
.await?
}
None => {
axum::Server::bind(&addr)
.serve(router.into_make_service())
.await?
let listener = tokio::net::TcpListener::bind(&addr).await?;
axum::serve(listener, router.into_make_service()).await?
}
}
@@ -418,7 +415,6 @@ fn get_ip() -> Option<String> {
/// Handle websockets
async fn ws_handler(
ws: WebSocketUpgrade,
_: Option<TypedHeader<headers::UserAgent>>,
Extension(state): Extension<Arc<WsReloadState>>,
) -> impl IntoResponse {
ws.on_upgrade(|mut socket| async move {
+74 -73
View File
@@ -1,13 +1,19 @@
use crate::Result;
use dioxus_cli_config::WebProxyConfig;
use anyhow::Context;
use anyhow::{anyhow, Context};
use axum::{http::StatusCode, routing::any, Router};
use hyper::{Request, Response, Uri};
use hyper_util::{
client::legacy::{self, connect::HttpConnector},
rt::TokioExecutor,
};
use axum::body::Body as MyBody;
#[derive(Debug, Clone)]
struct ProxyClient {
inner: hyper::Client<hyper_rustls::HttpsConnector<hyper::client::HttpConnector>>,
inner: legacy::Client<hyper_rustls::HttpsConnector<HttpConnector>, MyBody>,
url: Uri,
}
@@ -15,19 +21,17 @@ impl ProxyClient {
fn new(url: Uri) -> Self {
let https = hyper_rustls::HttpsConnectorBuilder::new()
.with_native_roots()
.unwrap()
.https_or_http()
.enable_http1()
.build();
Self {
inner: hyper::Client::builder().build(https),
inner: legacy::Client::builder(TokioExecutor::new()).build(https),
url,
}
}
async fn send(
&self,
mut req: Request<hyper::body::Body>,
) -> Result<Response<hyper::body::Body>> {
async fn send(&self, mut req: Request<MyBody>) -> Result<Response<hyper::body::Incoming>> {
let mut uri_parts = req.uri().clone().into_parts();
uri_parts.authority = self.url.authority().cloned();
uri_parts.scheme = self.url.scheme().cloned();
@@ -35,7 +39,7 @@ impl ProxyClient {
self.inner
.request(req)
.await
.map_err(crate::error::Error::ProxyRequestError)
.map_err(|err| crate::error::Error::Other(anyhow!(err)))
}
}
@@ -49,7 +53,7 @@ impl ProxyClient {
pub fn add_proxy(mut router: Router, proxy: &WebProxyConfig) -> Result<Router> {
let url: Uri = proxy.backend.parse()?;
let path = url.path().to_string();
let trimmed_path = path.trim_end_matches('/');
let trimmed_path = path.trim_start_matches('/');
if trimmed_path.is_empty() {
return Err(crate::Error::ProxySetupError(format!(
@@ -61,14 +65,11 @@ pub fn add_proxy(mut router: Router, proxy: &WebProxyConfig) -> Result<Router> {
let client = ProxyClient::new(url);
// We also match everything after the path using a wildcard matcher.
let wildcard_client = client.clone();
router = router.route(
// Always remove trailing /'s so that the exact route
// matches.
trimmed_path,
any(move |req| async move {
&format!("/*{}", trimmed_path.trim_end_matches('/')),
any(move |req: Request<MyBody>| async move {
client
.send(req)
.await
@@ -76,19 +77,6 @@ pub fn add_proxy(mut router: Router, proxy: &WebProxyConfig) -> Result<Router> {
}),
);
// Wildcard match anything else _after_ the backend URL's path.
// Note that we know `path` ends with a trailing `/` in this branch,
// so `wildcard` will look like `http://localhost/api/*proxywildcard`.
let wildcard = format!("{}/*proxywildcard", trimmed_path);
router = router.route(
&wildcard,
any(move |req| async move {
wildcard_client
.send(req)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))
}),
);
Ok(router)
}
@@ -97,30 +85,47 @@ mod test {
use super::*;
use axum::{extract::Path, Router};
use axum::Router;
use axum_server::{Handle, Server};
fn setup_servers(
mut config: WebProxyConfig,
) -> (
tokio::task::JoinHandle<()>,
tokio::task::JoinHandle<()>,
String,
) {
let backend_router = Router::new().route(
"/*path",
any(|path: Path<String>| async move { format!("backend: {}", path.0) }),
);
let backend_server = axum::Server::bind(&"127.0.0.1:0".parse().unwrap())
.serve(backend_router.into_make_service());
let backend_addr = backend_server.local_addr();
let backend_handle = tokio::spawn(async move { backend_server.await.unwrap() });
async fn setup_servers(mut config: WebProxyConfig) -> String {
let backend_router =
Router::new().route(
"/*path",
any(|request: axum::extract::Request| async move {
format!("backend: {}", request.uri())
}),
);
// The API backend server
let backend_handle_handle = Handle::new();
let backend_handle_handle_ = backend_handle_handle.clone();
tokio::spawn(async move {
Server::bind("127.0.0.1:0".parse().unwrap())
.handle(backend_handle_handle_)
.serve(backend_router.into_make_service())
.await
.unwrap();
});
// Set the user's config to this dummy API we just built so we can test it
let backend_addr = backend_handle_handle.listening().await.unwrap();
config.backend = format!("http://{}{}", backend_addr, config.backend);
// Now set up our actual filesystem server
let router = super::add_proxy(Router::new(), &config);
let server = axum::Server::bind(&"127.0.0.1:0".parse().unwrap())
.serve(router.unwrap().into_make_service());
let server_addr = server.local_addr();
let server_handle = tokio::spawn(async move { server.await.unwrap() });
(backend_handle, server_handle, server_addr.to_string())
let server_handle_handle = Handle::new();
let server_handle_handle_ = server_handle_handle.clone();
tokio::spawn(async move {
Server::bind("127.0.0.1:0".parse().unwrap())
.handle(server_handle_handle_)
.serve(router.unwrap().into_make_service())
.await
.unwrap();
});
// Expose *just* the fileystem web server's address
server_handle_handle.listening().await.unwrap().to_string()
}
async fn test_proxy_requests(path: String) {
@@ -132,42 +137,38 @@ mod test {
// So in day to day usage, use `http://localhost:8000/api` instead!
backend: path,
};
let (backend_handle, server_handle, server_addr) = setup_servers(config);
let resp = hyper::Client::new()
.get(format!("http://{}/api", server_addr).parse().unwrap())
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let server_addr = setup_servers(config).await;
assert_eq!(
hyper::body::to_bytes(resp.into_body()).await.unwrap(),
reqwest::get(format!("http://{}/api", server_addr))
.await
.unwrap()
.text()
.await
.unwrap(),
"backend: /api"
);
let resp = hyper::Client::new()
.get(format!("http://{}/api/", server_addr).parse().unwrap())
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
hyper::body::to_bytes(resp.into_body()).await.unwrap(),
reqwest::get(format!("http://{}/api/", server_addr))
.await
.unwrap()
.text()
.await
.unwrap(),
"backend: /api/"
);
let resp = hyper::Client::new()
.get(
format!("http://{}/api/subpath", server_addr)
.parse()
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
hyper::body::to_bytes(resp.into_body()).await.unwrap(),
reqwest::get(format!("http://{server_addr}/api/subpath"))
.await
.unwrap()
.text()
.await
.unwrap(),
"backend: /api/subpath"
);
backend_handle.abort();
server_handle.abort();
}
#[tokio::test]
+2 -7
View File
@@ -49,17 +49,12 @@ mobile = ["dioxus-mobile", "dioxus-fullstack?/mobile", "dioxus-config-macro/mobi
web = ["dioxus-web", "dioxus-fullstack?/web", "dioxus-config-macro/web", "dioxus-router?/web"]
ssr = ["dioxus-ssr", "dioxus-router?/ssr", "dioxus-config-macro/ssr"]
liveview = ["dioxus-liveview", "dioxus-config-macro/liveview", "dioxus-router?/liveview"]
axum = ["dioxus-fullstack?/axum", "ssr", "dioxus-liveview?/axum"]
salvo = ["dioxus-fullstack?/salvo", "ssr", "dioxus-liveview?/salvo"]
warp = ["dioxus-fullstack?/warp", "ssr", "dioxus-liveview?/warp"]
rocket = ["dioxus-liveview?/rocket"]
axum = ["dioxus-fullstack?/axum", "dioxus-fullstack?/server", "ssr", "dioxus-liveview?/axum"]
tui = ["dioxus-tui", "dioxus-config-macro/tui"]
# This feature just disables the no-renderer-enabled warning
third-party-renderer = []
# This feature enables some nightly flags that make it more clear what structs/methods are available in each feature
nightly-doc = []
[dev-dependencies]
futures-util = { workspace = true }
tracing = { workspace = true }
+2 -2
View File
@@ -5,8 +5,8 @@ fn main() {
return;
}
let liveview_renderers = ["liveview", "axum", "salvo", "warp", "rocket"];
let fullstack_renderers = ["axum", "salvo", "warp"];
let liveview_renderers = ["liveview", "axum"];
let fullstack_renderers = ["axum"];
let client_renderers = ["desktop", "mobile", "web", "tui"];
let client_renderer_selected = client_renderers
.iter()
+9 -9
View File
@@ -38,7 +38,7 @@ impl LaunchBuilder {
/// Launch your web application.
#[cfg(feature = "web")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "web")))]
#[cfg_attr(docsrs, doc(cfg(feature = "web")))]
pub fn web() -> LaunchBuilder<dioxus_web::Config, UnsendContext> {
LaunchBuilder {
launch_fn: dioxus_web::launch::launch,
@@ -49,7 +49,7 @@ impl LaunchBuilder {
/// Launch your desktop application.
#[cfg(feature = "desktop")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "desktop")))]
#[cfg_attr(docsrs, doc(cfg(feature = "desktop")))]
pub fn desktop() -> LaunchBuilder<dioxus_desktop::Config, UnsendContext> {
LaunchBuilder {
launch_fn: dioxus_desktop::launch::launch,
@@ -60,7 +60,7 @@ impl LaunchBuilder {
/// Launch your fullstack application.
#[cfg(feature = "fullstack")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "fullstack")))]
#[cfg_attr(docsrs, doc(cfg(feature = "fullstack")))]
pub fn fullstack() -> LaunchBuilder<dioxus_fullstack::Config, SendContext> {
LaunchBuilder {
launch_fn: dioxus_fullstack::launch::launch,
@@ -71,7 +71,7 @@ impl LaunchBuilder {
/// Launch your fullstack application.
#[cfg(feature = "mobile")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "mobile")))]
#[cfg_attr(docsrs, doc(cfg(feature = "mobile")))]
pub fn mobile() -> LaunchBuilder<dioxus_mobile::Config, UnsendContext> {
LaunchBuilder {
launch_fn: dioxus_mobile::launch::launch,
@@ -81,7 +81,7 @@ impl LaunchBuilder {
}
#[cfg(feature = "tui")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "tui")))]
#[cfg_attr(docsrs, doc(cfg(feature = "tui")))]
/// Launch your tui application
pub fn tui() -> LaunchBuilder<dioxus_tui::Config, UnsendContext> {
LaunchBuilder {
@@ -232,28 +232,28 @@ pub fn launch(app: fn() -> Element) {
}
#[cfg(feature = "web")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "web")))]
#[cfg_attr(docsrs, doc(cfg(feature = "web")))]
/// Launch your web application without any additional configuration. See [`LaunchBuilder`] for more options.
pub fn launch_web(app: fn() -> Element) {
LaunchBuilder::web().launch(app)
}
#[cfg(feature = "desktop")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "desktop")))]
#[cfg_attr(docsrs, doc(cfg(feature = "desktop")))]
/// Launch your desktop application without any additional configuration. See [`LaunchBuilder`] for more options.
pub fn launch_desktop(app: fn() -> Element) {
LaunchBuilder::desktop().launch(app)
}
#[cfg(feature = "fullstack")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "fullstack")))]
#[cfg_attr(docsrs, doc(cfg(feature = "fullstack")))]
/// Launch your fullstack application without any additional configuration. See [`LaunchBuilder`] for more options.
pub fn launch_fullstack(app: fn() -> Element) {
LaunchBuilder::fullstack().launch(app)
}
#[cfg(feature = "tui")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "tui")))]
#[cfg_attr(docsrs, doc(cfg(feature = "tui")))]
/// Launch your tui application without any additional configuration. See [`LaunchBuilder`] for more options.
pub fn launch_tui(app: fn() -> Element) {
LaunchBuilder::tui().launch(app)
+27 -26
View File
@@ -1,114 +1,115 @@
#![doc = include_str!("../README.md")]
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
#![cfg_attr(any(docsrs, feature = "nightly-doc"), feature(doc_cfg))]
#![cfg_attr(docsrs, feature(doc_cfg))]
pub use dioxus_core;
#[cfg(feature = "launch")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "launch")))]
#[cfg_attr(docsrs, doc(cfg(feature = "launch")))]
mod launch;
#[cfg(feature = "hooks")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "hooks")))]
#[cfg_attr(docsrs, doc(cfg(feature = "hooks")))]
pub use dioxus_hooks as hooks;
#[cfg(feature = "signals")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "signals")))]
#[cfg_attr(docsrs, doc(cfg(feature = "signals")))]
pub use dioxus_signals as signals;
pub mod events {
#[cfg(feature = "html")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "html")))]
#[cfg_attr(docsrs, doc(cfg(feature = "html")))]
pub use dioxus_html::prelude::*;
}
#[cfg(feature = "html")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "html")))]
#[cfg_attr(docsrs, doc(cfg(feature = "html")))]
pub use dioxus_html as html;
#[cfg(feature = "macro")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "macro")))]
#[cfg_attr(docsrs, doc(cfg(feature = "macro")))]
pub use dioxus_core_macro as core_macro;
pub mod prelude {
#[cfg(feature = "launch")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "launch")))]
#[cfg_attr(docsrs, doc(cfg(feature = "launch")))]
pub use crate::launch::*;
#[cfg(feature = "hooks")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "hooks")))]
#[cfg_attr(docsrs, doc(cfg(feature = "hooks")))]
pub use crate::hooks::*;
#[cfg(feature = "signals")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "signals")))]
#[cfg_attr(docsrs, doc(cfg(feature = "signals")))]
pub use dioxus_signals::*;
pub use dioxus_core::prelude::*;
#[cfg(feature = "macro")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "macro")))]
#[cfg_attr(docsrs, doc(cfg(feature = "macro")))]
#[allow(deprecated)]
pub use dioxus_core_macro::{component, format_args_f, inline_props, render, rsx, Props};
#[cfg(feature = "launch")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "launch")))]
#[cfg_attr(docsrs, doc(cfg(feature = "launch")))]
pub use dioxus_config_macro::*;
#[cfg(feature = "html")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "html")))]
#[cfg_attr(docsrs, doc(cfg(feature = "html")))]
pub use dioxus_html as dioxus_elements;
#[cfg(feature = "html")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "html")))]
#[cfg_attr(docsrs, doc(cfg(feature = "html")))]
pub use dioxus_elements::{prelude::*, GlobalAttributes, SvgAttributes};
#[cfg(all(not(target_arch = "wasm32"), feature = "hot-reload"))]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "hot-reload")))]
#[cfg_attr(docsrs, doc(cfg(feature = "hot-reload")))]
pub use dioxus_hot_reload::{self, hot_reload_init};
pub use dioxus_core;
#[cfg(feature = "fullstack")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "fullstack")))]
#[cfg_attr(docsrs, doc(cfg(feature = "fullstack")))]
pub use dioxus_fullstack::prelude::*;
#[cfg(feature = "router")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "router")))]
#[cfg_attr(docsrs, doc(cfg(feature = "router")))]
pub use dioxus_router;
#[cfg(feature = "router")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "router")))]
#[cfg_attr(docsrs, doc(cfg(feature = "router")))]
pub use dioxus_router::prelude::*;
}
#[cfg(feature = "web")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "web")))]
#[cfg_attr(docsrs, doc(cfg(feature = "web")))]
pub use dioxus_web as web;
#[cfg(feature = "router")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "router")))]
#[cfg_attr(docsrs, doc(cfg(feature = "router")))]
pub use dioxus_router as router;
#[cfg(feature = "fullstack")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "fullstack")))]
#[cfg_attr(docsrs, doc(cfg(feature = "fullstack")))]
pub use dioxus_fullstack as fullstack;
#[cfg(feature = "desktop")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "desktop")))]
#[cfg_attr(docsrs, doc(cfg(feature = "desktop")))]
pub use dioxus_desktop as desktop;
#[cfg(feature = "mobile")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "mobile")))]
#[cfg_attr(docsrs, doc(cfg(feature = "mobile")))]
pub use dioxus_desktop as mobile;
#[cfg(feature = "liveview")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "liveview")))]
#[cfg_attr(docsrs, doc(cfg(feature = "liveview")))]
pub use dioxus_liveview as liveview;
#[cfg(feature = "tui")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "tui")))]
#[cfg_attr(docsrs, doc(cfg(feature = "tui")))]
pub use dioxus_tui as tui;
#[cfg(feature = "ssr")]
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "ssr")))]
#[cfg_attr(docsrs, doc(cfg(feature = "ssr")))]
pub use dioxus_ssr as ssr;
+26 -20
View File
@@ -12,26 +12,19 @@ resolver = "2"
[dependencies]
# server functions
server_fn = { version = "0.5.2", default-features = false }
dioxus_server_macro = { workspace = true }
# warp
warp = { version = "0.3.5", features = ["compression-gzip"], optional = true }
server_fn = { version = "0.6.5", features = ["json", "url", "browser"], default-features = false }
dioxus_server_macro = { workspace = true, version = "0.6.5", default-features = false }
# axum
axum = { version = "0.6.1", features = ["ws", "macros"], default-features = false, optional = true }
tower-http = { version = "0.4.0", optional = true, features = ["fs", "compression-gzip"] }
# salvo
salvo = { version = "0.63.0", optional = true, features = ["serve-static", "websocket", "compression"] }
http-body-util = { version = "0.1.0-rc.2", optional = true }
axum = { workspace = true, features = ["ws", "macros"], default-features = false, optional = true }
tower-http = { workspace = true, optional = true, features = ["fs", "compression-gzip"] }
dioxus-lib = { workspace = true }
# Dioxus + SSR
dioxus-ssr = { workspace = true, optional = true }
hyper = { version = "0.14.25", optional = true }
http = { version = "0.2.9", optional = true }
hyper = { workspace = true, optional = true }
http = { workspace = true, optional = true }
# Web Integration
dioxus-web = { workspace = true, features = ["hydrate"], optional = true }
@@ -59,7 +52,7 @@ pin-project = { version = "1.1.2", optional = true }
thiserror = { workspace = true, optional = true }
async-trait = "0.1.71"
bytes = "1.4.0"
tower = { version = "0.4.13", features = ["util"], optional = true }
tower = { workspace = true, features = ["util"], optional = true }
tower-layer = { version = "0.3.2", optional = true }
web-sys = { version = "0.3.61", optional = true, features = ["Window", "Document", "Element", "HtmlDocument", "Storage", "console"] }
@@ -78,11 +71,24 @@ hot-reload = ["serde_json"]
web = ["dioxus-web", "web-sys"]
desktop = ["dioxus-desktop"]
mobile = ["dioxus-mobile"]
warp = ["dep:warp", "server"]
axum = ["dep:axum", "tower-http", "server"]
salvo = ["dep:salvo", "server", "http-body-util"]
server = ["server_fn/ssr", "dioxus_server_macro/server", "tokio", "tokio-util", "tokio-stream", "dioxus-ssr", "dioxus-ssr/incremental", "tower", "hyper", "http", "tower-layer", "anymap", "tracing-futures", "pin-project", "thiserror", "dioxus-cli-config"]
default-tls = ["server_fn/default-tls"]
rustls = ["server_fn/rustls"]
# This feature enables some nightly flags that make it more clear what structs/methods are available in each feature
nightly-doc = []
axum = ["dep:axum", "tower-http", "server", "server_fn/axum", "dioxus_server_macro/axum"]
server = [
"server_fn/ssr",
"dioxus_server_macro/server",
"tokio",
"tokio-util",
"tokio-stream",
"dioxus-ssr",
"dioxus-ssr/incremental",
"tower",
"hyper",
"http",
"tower-layer",
"anymap",
"tracing-futures",
"pin-project",
"thiserror",
"dioxus-cli-config"
]
+19 -27
View File
@@ -37,51 +37,43 @@ Full stack Dioxus in under 50 lines of code
use dioxus::prelude::*;
use dioxus_fullstack::prelude::*;
// On the web, run our client code
#[cfg(feature = "web")]
fn main() {
#[cfg(feature = "web")]
dioxus_web::launch_with_props(
app,
get_root_props_from_document().unwrap_or_default(),
dioxus_web::Config::new().hydrate(true),
);
#[cfg(feature = "server")]
{
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async move {
warp::serve(
// Automatically handles server side rendering, hot reloading intigration, and hosting server functions
serve_dioxus_application(
"",
ServerConfig::new(app, ()),
)
)
.run(([127, 0, 0, 1], 8080))
.await;
});
}
}
// On the server, run a simple warp server
#[cfg(feature = "server")]
#[tokio::main]
async fn main() {
// Automatically handles server side rendering, hot reloading intigration, and hosting server functions
warp::serve(serve_dioxus_application("", ServerConfig::new(app, ())))
.run(([127, 0, 0, 1], 8080))
.await;
}
fn app() -> Element {
let meaning = use_signal(|| None);
rsx! {
h1 { "Meaning of life: {meaning:?}" }
button {
onclick: move |_| {
to_owned![meaning];
async move {
if let Ok(data) = get_meaning("life the universe and everything".into()).await {
meaning.set(data);
}
onclick: move |_| async move {
if let Ok(data) = get_meaning("life the universe and everything".into()).await {
meaning.set(data);
}
},
"Run a server function"
}
"Server said: {meaning:?}"
})
}
}
// This code will only run on the server
#[server(GetMeaning)]
#[server]
async fn get_meaning(of: String) -> Result<Option<u32>, ServerFnError> {
Ok(of.contains("life").then(|| 42))
}
@@ -8,13 +8,11 @@ publish = false
[dependencies]
dioxus-web = { workspace = true, features = ["hydrate"], optional = true }
dioxus = { workspace = true }
dioxus = { features = ["fullstack"], workspace = true }
dioxus-fullstack = { workspace = true }
axum = { version = "0.6.12", optional = true }
axum = { workspace = true, optional = true }
tokio = { workspace = true, features = ["full"], optional = true }
serde = "1.0.159"
execute = "0.2.12"
tower-http = { version = "0.4.1", features = ["auth"], optional = true }
tower-http = { workspace = true, features = ["auth"], optional = true }
simple_logger = { version = "4.2.0", optional = true }
async-trait = { version = "0.1.71", optional = true }
sqlx = { version = "0.7.0", features = [
@@ -26,17 +24,21 @@ sqlx = { version = "0.7.0", features = [
"tls-rustls",
"runtime-tokio",
], optional = true }
http = { workspace = true, optional = true }
tower = { workspace = true, optional = true }
serde = "1.0.159"
execute = "0.2.12"
anyhow = "1.0.71"
http = { version = "0.2.9", optional = true }
tower = { version = "0.4.13", optional = true }
[dependencies.axum_session]
version = "0.3.0"
workspace = true
features = ["sqlite-rustls"]
optional = true
[dependencies.axum_session_auth]
version = "0.3.0"
workspace = true
features = ["sqlite-rustls"]
optional = true
@@ -17,7 +17,7 @@ use serde::{Deserialize, Serialize};
fn main() {
#[cfg(feature = "web")]
// Hydrate the application on the client
dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true));
dioxus_web::launch::launch_cfg(app, dioxus_web::Config::new().hydrate(true));
#[cfg(feature = "server")]
{
@@ -44,14 +44,14 @@ fn main() {
.await
.unwrap();
//Create the Database table for storing our Session Data.
session_store.initiate().await.unwrap();
User::create_user_tables(&pool).await;
// build our application with some routes
let app = Router::new()
// Server side render the application, serve static assets, and register server functions
.serve_dioxus_application("", ServerConfig::new(app, ()))
.serve_dioxus_application(ServeConfig::builder().build(), || {
VirtualDom::new(app)
})
.layer(
axum_session_auth::AuthSessionLayer::<
crate::auth::User,
@@ -65,9 +65,9 @@ fn main() {
// run it
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 3000));
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::Server::bind(&addr)
.serve(app.into_make_service())
axum::serve(listener, app.into_make_service())
.await
.unwrap();
});
@@ -7,8 +7,8 @@ publish = false
[lib]
[dependencies]
dioxus = { workspace = true }
axum = { version = "0.6.12", optional = true }
dioxus = { workspace = true, features = ["launch", "fullstack"] }
axum = { workspace = true, optional = true }
tokio = { workspace = true, features = ["full"], optional = true }
serde = "1.0.159"
@@ -9,5 +9,7 @@ fn main() {
// Set the url of the server where server functions are hosted.
#[cfg(not(feature = "server"))]
dioxus::fullstack::prelude::server_fn::set_server_url("http://127.0.0.1:8080");
dioxus::desktop::launch(app)
#[cfg(feature = "desktop")]
dioxus::prelude::launch_desktop(app)
}
@@ -5,20 +5,23 @@
use axum_desktop::*;
use dioxus::prelude::*;
use server_fn::axum::register_explicit;
#[tokio::main]
async fn main() {
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
let _ = PostServerData::register_explicit();
let _ = GetServerData::register_explicit();
axum::Server::bind(&addr)
.serve(
axum::Router::new()
.register_server_fns("")
.into_make_service(),
)
let listener = tokio::net::TcpListener::bind("127.0.0.01:8080")
.await
.unwrap();
register_explicit::<PostServerData>();
register_explicit::<GetServerData>();
axum::serve(
listener,
axum::Router::new()
.register_server_fns()
.into_make_service(),
)
.await
.unwrap();
}
@@ -7,8 +7,8 @@ publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus = { workspace = true, features = ["router"] }
axum = { version = "0.6.12", optional = true }
dioxus = { workspace = true, features = ["fullstack", "router"] }
axum = { workspace = true, optional = true }
tokio = {workspace = true, features = ["full"], optional = true }
serde = { version = "1.0.159", features = ["derive"] }
@@ -16,3 +16,4 @@ serde = { version = "1.0.159", features = ["derive"] }
default = []
server = ["axum", "dioxus/axum"]
web = ["dioxus/web"]
@@ -7,21 +7,17 @@
use dioxus::prelude::*;
fn main() {
let config = LaunchBuilder::fullstack();
#[cfg(feature = "server")]
config
.incremental(
IncrementalRendererConfig::default()
.invalidate_after(std::time::Duration::from_secs(120)),
)
.launch();
let cfg = server_only!(dioxus::fullstack::Config::new().incremental(
IncrementalRendererConfig::default().invalidate_after(std::time::Duration::from_secs(120)),
));
#[cfg(not(feature = "server"))]
config.launch(|| {
rsx! {
Router::<Route> {}
}
});
LaunchBuilder::fullstack().with_cfg(cfg).launch(app);
}
fn app() -> Element {
rsx! {
Router::<Route> {}
}
}
#[derive(Clone, Routable, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
@@ -1,5 +1,5 @@
[package]
name = "warp-hello-world"
name = "axum-streaming"
version = "0.1.0"
edition = "2021"
publish = false
@@ -9,12 +9,16 @@ publish = false
[dependencies]
dioxus = { workspace = true, features = ["fullstack"] }
serde = "1.0.159"
simple_logger = "4.2.0"
tracing-wasm = "0.2.1"
tracing = { workspace = true }
tracing.workspace = true
tracing-subscriber = "0.3.17"
reqwest = "0.11.18"
futures = "0.3.30"
tokio = { workspace = true, optional = true }
futures-util.workspace = true
once_cell = "1.19.0"
[features]
default = []
server = ["dioxus/warp"]
server = ["dioxus/axum", "tokio"]
web = ["dioxus/web"]
@@ -0,0 +1,41 @@
use dioxus::prelude::*;
use futures::StreamExt;
use server_fn::codec::{StreamingText, TextStream};
fn app() -> Element {
let mut response = use_signal(String::new);
rsx! {
button {
onclick: move |_| async move {
response.write().clear();
if let Ok(stream) = test_stream().await {
response.write().push_str("Stream started\n");
let mut stream = stream.into_inner();
while let Some(Ok(text)) = stream.next().await {
response.write().push_str(&text);
}
}
},
"Start stream"
}
"{response}"
}
}
#[server(output = StreamingText)]
pub async fn test_stream() -> Result<TextStream, ServerFnError> {
let (tx, rx) = futures::channel::mpsc::unbounded();
tokio::spawn(async move {
loop {
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
let _ = tx.unbounded_send(Ok("Hello, world!".to_string()));
}
});
Ok(TextStream::new(rx))
}
fn main() {
launch(app)
}
@@ -1,25 +0,0 @@
[package]
name = "salvo-hello-world"
version = "0.1.0"
edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus-web = { workspace = true, features=["hydrate"], optional = true }
dioxus = { workspace = true }
dioxus-fullstack = { workspace = true }
serde = "1.0.159"
salvo = { version = "0.63.0", optional = true }
execute = "0.2.12"
reqwest = "0.11.18"
simple_logger = "4.2.0"
tracing-wasm = "0.2.1"
tracing = { workspace = true }
tracing-subscriber = "0.3.17"
[features]
default = []
server = ["salvo", "dioxus-fullstack/salvo"]
web = ["dioxus-web"]
@@ -1,57 +0,0 @@
//! Run with:
//!
//! ```sh
//! dx serve --platform fullstack
//! ```
#![allow(non_snake_case, unused)]
use dioxus::prelude::*;
use dioxus_fullstack::{launch, prelude::*};
use serde::{Deserialize, Serialize};
fn app() -> Element {
let state = use_server_future(move || async move { get_server_data().await.unwrap() })?;
let mut count = use_signal(|| 0);
let mut text = use_signal(|| "...".to_string());
rsx! {
div { "Server state: {state.value().unwrap()}" }
h1 { "High-Five counter: {count}" }
button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
button {
onclick: move |_| async move {
if let Ok(data) = get_server_data().await {
println!("Client received: {}", data);
text.set(data.clone());
post_server_data(data).await.unwrap();
}
},
"Run a server function!"
}
"Server said: {text}"
}
}
#[server(PostServerData)]
async fn post_server_data(data: String) -> Result<(), ServerFnError> {
println!("Server received: {}", data);
Ok(())
}
#[server(GetServerData)]
async fn get_server_data() -> Result<String, ServerFnError> {
Ok(reqwest::get("https://httpbin.org/ip").await?.text().await?)
}
fn main() {
#[cfg(feature = "web")]
tracing_wasm::set_as_global_default();
#[cfg(feature = "server")]
tracing_subscriber::fmt::init();
LaunchBuilder::fullstack().launch(app);
}
@@ -14,16 +14,25 @@ use serde::{Deserialize, Serialize};
#[cfg(feature = "server")]
#[tokio::main]
async fn main() {
pre_cache_static_routes_with_props(
&ServerConfig::new_with_router(
dioxus_fullstack::router::FullstackRouterConfig::<Route>::default(),
)
.assets_path("docs")
.incremental(IncrementalRendererConfig::default().static_dir("docs"))
.build(),
)
.await
.unwrap();
let wrapper = DefaultRenderer {
before_body: r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,
initial-scale=1.0">
<title>Dioxus Application</title>
</head>
<body>"#
.to_string(),
after_body: r#"</body>
</html>"#
.to_string(),
};
let mut renderer = IncrementalRenderer::builder().build();
pre_cache_static_routes::<Route, _>(&mut renderer, &wrapper)
.await
.unwrap();
}
// Hydrate the page
@@ -37,13 +46,11 @@ fn main() {
);
}
#[cfg(not(any(feature = "web", feature = "server")))]
fn main() {}
#[derive(Clone, Routable, Debug, PartialEq, Serialize, Deserialize)]
enum Route {
#[route("/")]
Home {},
#[route("/blog")]
Blog,
}
@@ -72,10 +79,7 @@ fn Home() -> Element {
let text = use_signal(|| "...".to_string());
rsx! {
Link {
to: Route::Blog {},
"Go to blog"
}
Link { to: Route::Blog {}, "Go to blog" }
div {
h1 { "High-Five counter: {count}" }
button { onclick: move |_| count += 1, "Up high!" }
@@ -1,4 +0,0 @@
dist
target
static
.dioxus
@@ -1,54 +0,0 @@
//! Run with:
//!
//! ```sh
//! dx serve --platform fullstack
//! ```
#![allow(non_snake_case, unused)]
use dioxus::prelude::*;
fn app() -> Element {
let mut count = use_signal(|| 0);
let text = use_signal(|| "...".to_string());
rsx! {
h1 { "High-Five counter: {count}" }
button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
button {
onclick: move |_| {
to_owned![text];
async move {
if let Ok(data) = get_server_data().await {
println!("Client received: {}", data);
text.set(data.clone());
post_server_data(data).await.unwrap();
}
}
},
"Run a server function!"
}
"Server said: {text}"
}
}
#[server]
async fn post_server_data(data: String) -> Result<(), ServerFnError> {
println!("Server received: {}", data);
Ok(())
}
#[server]
async fn get_server_data() -> Result<String, ServerFnError> {
Ok(reqwest::get("https://httpbin.org/ip").await?.text().await?)
}
fn main() {
#[cfg(feature = "web")]
tracing_wasm::set_as_global_default();
#[cfg(feature = "server")]
tracing_subscriber::fmt::init();
launch(app);
}
-193
View File
@@ -1,193 +0,0 @@
//! # Adapters
//! Adapters for different web frameworks.
//!
//! Each adapter provides a set of utilities that is ergonomic to use with the framework.
//!
//! Each framework has utilies for some or all of the following:
//! - Server functions
//! - A generic way to register server functions
//! - A way to register server functions with a custom handler that allows users to pass in a custom [`crate::server_context::DioxusServerContext`] based on the state of the server framework.
//! - A way to register static WASM files that is accepts [`crate::serve_config::ServeConfig`]
//! - A hot reloading web socket that intigrates with [`dioxus-hot-reload`](https://crates.io/crates/dioxus-hot-reload)
#[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::future::Future;
use std::sync::{Arc, RwLock};
use tokio::task::JoinError;
use crate::{
layer::{BoxedService, Service},
prelude::{DioxusServerContext, ProvideServerContext},
};
/// Create a server function handler with the given server context and server function.
pub fn server_fn_service(
context: DioxusServerContext,
function: server_fn::ServerFnTraitObj<()>,
) -> crate::layer::BoxedService {
let prefix = function.prefix().to_string();
let url = function.url().to_string();
if let Some(middleware) = crate::server_fn::MIDDLEWARE.get(&(&prefix, &url)) {
let mut service = BoxedService(Box::new(ServerFnHandler::new(context, function)));
for middleware in middleware {
service = middleware.layer(service);
}
service
} else {
BoxedService(Box::new(ServerFnHandler::new(context, function)))
}
}
#[derive(Clone)]
/// 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,
}
}
}
impl Service for ServerFnHandler {
fn run(
&mut self,
req: http::Request<hyper::body::Body>,
) -> std::pin::Pin<
Box<
dyn std::future::Future<
Output = Result<http::Response<hyper::body::Body>, server_fn::ServerFnError>,
> + Send,
>,
> {
let Self {
server_context,
function,
} = self.clone();
let f = 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 headers = &parts.headers;
let accept_header = headers.get("Accept").cloned();
let parts = Arc::new(RwLock::new(parts));
// Because the future returned by `server_fn_handler` is `Send`, and the future returned by this function must be send, we need to spawn a new runtime
let result = spawn_platform({
let function = function.clone();
let mut server_context = server_context.clone();
server_context.parts = parts;
move || async move {
let data = match function.encoding() {
Encoding::Url | Encoding::Cbor => &body,
Encoding::GetJSON | Encoding::GetCBOR => &query,
};
let server_function_future = function.call((), data);
let server_function_future =
ProvideServerContext::new(server_function_future, server_context.clone());
server_function_future.await
}
})
.await?;
let mut res = http::Response::builder();
// Set the headers from the server context
let parts = server_context.response_parts().unwrap();
*res.headers_mut().expect("empty headers should be valid") = parts.headers.clone();
let serialized = result?;
// if this is Accept: application/json then send a serialized JSON response
let accept_header = accept_header.as_ref().and_then(|value| value.to_str().ok());
if accept_header == Some("application/json")
|| accept_header
== Some(
"application/\
x-www-form-urlencoded",
)
|| accept_header == Some("application/cbor")
{
res = res.status(StatusCode::OK);
}
Ok(match serialized {
Payload::Binary(data) => {
res = res.header("Content-Type", "application/cbor");
res.body(data.into())?
}
Payload::Url(data) => {
res = res.header(
"Content-Type",
"application/\
x-www-form-urlencoded",
);
res.body(data.into())?
}
Payload::Json(data) => {
res = res.header("Content-Type", "application/json");
res.body(data.into())?
}
})
};
#[cfg(not(target_arch = "wasm32"))]
{
Box::pin(f)
}
#[cfg(target_arch = "wasm32")]
{
use futures_util::future::FutureExt;
let result = tokio::task::spawn_local(f);
let result = result.then(|f| async move { f.unwrap() });
Box::pin(result)
}
}
}
async fn spawn_platform<F, Fut>(create_task: F) -> Result<<Fut as Future>::Output, JoinError>
where
F: FnOnce() -> Fut,
F: Send + 'static,
Fut: Future + 'static,
Fut::Output: Send + 'static,
{
#[cfg(not(target_arch = "wasm32"))]
{
get_local_pool().spawn_pinned(create_task).await
}
#[cfg(target_arch = "wasm32")]
{
Ok(create_task().await)
}
}
fn get_local_pool() -> tokio_util::task::LocalPoolHandle {
use once_cell::sync::OnceCell;
static LOCAL_POOL: OnceCell<tokio_util::task::LocalPoolHandle> = OnceCell::new();
LOCAL_POOL
.get_or_init(|| {
tokio_util::task::LocalPoolHandle::new(
std::thread::available_parallelism()
.map(Into::into)
.unwrap_or(1),
)
})
.clone()
}
@@ -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)
}
}
@@ -54,64 +54,26 @@
//! }
//! ```
use axum::routing::*;
use axum::{
body::{self, Body, BoxBody},
body::{self, Body},
extract::State,
handler::Handler,
http::{Request, Response, StatusCode},
response::IntoResponse,
routing::{get, post},
Router,
};
use dioxus_lib::prelude::VirtualDom;
use server_fn::{Encoding, ServerFunctionRegistry};
use http::header::*;
use std::sync::Arc;
use std::sync::RwLock;
use crate::{
prelude::*, render::SSRState, serve_config::ServeConfig, server_context::DioxusServerContext,
server_fn::DioxusServerFnRegistry,
};
/// A extension trait with utilities for integrating Dioxus with your Axum router.
pub trait DioxusRouterExt<S> {
/// 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 dioxus_lib::prelude::*;
/// use dioxus_fullstack::prelude::*;
///
/// #[tokio::main]
/// async fn main() {
/// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
/// axum::Server::bind(&addr)
/// .serve(
/// axum::Router::new()
/// .register_server_fns_with_handler("", |func| {
/// move |req: Request<Body>| async move {
/// let (parts, body) = req.into_parts();
/// let parts: Arc<http::request::Parts> = Arc::new(parts.into());
/// let server_context = DioxusServerContext::new(parts.clone());
/// server_fn_handler(server_context, func.clone(), parts, body).await
/// }
/// })
/// .into_make_service(),
/// )
/// .await
/// .unwrap();
/// }
/// ```
fn register_server_fns_with_handler<H, T>(
self,
server_fn_route: &'static str,
handler: impl FnMut(server_fn::ServerFnTraitObj<()>) -> H,
) -> Self
where
H: Handler<T, S>,
T: 'static,
S: Clone + Send + Sync + 'static;
/// Registers server functions with the default handler. This handler function will pass an empty [`DioxusServerContext`] to your server functions.
///
/// # Example
@@ -133,7 +95,7 @@ pub trait DioxusRouterExt<S> {
/// .unwrap();
/// }
/// ```
fn register_server_fns(self, server_fn_route: &'static str) -> Self;
fn register_server_fns(self) -> 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`].
///
@@ -218,7 +180,6 @@ pub trait DioxusRouterExt<S> {
/// ```
fn serve_dioxus_application(
self,
server_fn_route: &'static str,
cfg: impl Into<ServeConfig>,
build_virtual_dom: impl Fn() -> VirtualDom + Send + Sync + 'static,
) -> Self;
@@ -228,51 +189,21 @@ impl<S> DioxusRouterExt<S> for Router<S>
where
S: Send + Sync + Clone + 'static,
{
fn register_server_fns_with_handler<H, T>(
self,
server_fn_route: &'static str,
mut handler: impl FnMut(server_fn::ServerFnTraitObj<()>) -> H,
) -> Self
where
H: Handler<T, S, Body>,
T: 'static,
S: Clone + Send + Sync + '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.route(&full_route, post(handler(func)));
}
Encoding::GetJSON | Encoding::GetCBOR => {
router = router.route(&full_route, get(handler(func)));
}
}
}
router
}
fn register_server_fns(mut self) -> Self {
use http::method::Method;
fn register_server_fns(self, server_fn_route: &'static str) -> Self {
self.register_server_fns_with_handler(server_fn_route, |func| {
move |req: Request<Body>| {
let mut service = crate::server_fn_service(Default::default(), func);
async move {
let (req, body) = req.into_parts();
let req = Request::from_parts(req, body);
let res = service.run(req);
match res.await {
Ok(res) => Ok::<_, std::convert::Infallible>(res.map(|b| b.into())),
Err(e) => {
let mut res = Response::new(Body::from(e.to_string()));
*res.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
Ok(res)
}
}
}
}
})
for (path, method) in server_fn::axum::server_fn_paths() {
tracing::trace!("Registering server function: {} {}", method, path);
let handler = move |req| handle_server_fns_inner(path, || {}, 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)),
_ => todo!(),
};
}
self
}
fn serve_static_assets(mut self, assets_path: impl Into<std::path::PathBuf>) -> Self {
@@ -317,7 +248,6 @@ where
fn serve_dioxus_application(
self,
server_fn_route: &'static str,
cfg: impl Into<ServeConfig>,
build_virtual_dom: impl Fn() -> VirtualDom + Send + Sync + 'static,
) -> Self {
@@ -327,7 +257,7 @@ where
// Add server functions and render index.html
self.serve_static_assets(cfg.assets_path.clone())
.connect_hot_reload()
.register_server_fns(server_fn_route)
.register_server_fns()
.fallback(get(render_handler).with_state((cfg, Arc::new(build_virtual_dom), ssr_state)))
}
@@ -371,6 +301,13 @@ fn apply_request_parts_to_response<B>(
}
}
type AxumHandler<F> = (
F,
ServeConfig,
SSRState,
Arc<dyn Fn() -> VirtualDom + Send + Sync>,
);
/// SSR renderer handler for Axum with added context injection.
///
/// # Example
@@ -419,17 +356,13 @@ fn apply_request_parts_to_response<B>(
/// }
/// ```
pub async fn render_handler_with_context<F: FnMut(&mut DioxusServerContext)>(
State((mut inject_context, cfg, ssr_state, virtual_dom_factory)): State<(
F,
ServeConfig,
SSRState,
Arc<dyn Fn() -> VirtualDom + Send + Sync>,
)>,
State((mut inject_context, cfg, ssr_state, virtual_dom_factory)): State<AxumHandler<F>>,
request: Request<Body>,
) -> impl IntoResponse {
let (parts, _) = request.into_parts();
let url = parts.uri.path_and_query().unwrap().to_string();
let parts: Arc<RwLock<http::request::Parts>> = Arc::new(RwLock::new(parts.into()));
let parts: Arc<tokio::sync::RwLock<http::request::Parts>> =
Arc::new(tokio::sync::RwLock::new(parts));
let mut server_context = DioxusServerContext::new(parts.clone());
inject_context(&mut server_context);
@@ -452,13 +385,15 @@ pub async fn render_handler_with_context<F: FnMut(&mut DioxusServerContext)>(
}
}
type RenderHandlerExtractor = (
ServeConfig,
Arc<dyn Fn() -> VirtualDom + Send + Sync>,
SSRState,
);
/// SSR renderer handler for Axum
pub async fn render_handler(
State((cfg, virtual_dom_factory, ssr_state)): State<(
ServeConfig,
Arc<dyn Fn() -> VirtualDom + Send + Sync>,
SSRState,
)>,
State((cfg, virtual_dom_factory, ssr_state)): State<RenderHandlerExtractor>,
request: Request<Body>,
) -> impl IntoResponse {
render_handler_with_context(
@@ -468,10 +403,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()
}
@@ -516,3 +451,98 @@ pub async fn hot_reload_handler(ws: axum::extract::WebSocketUpgrade) -> impl Int
}
})
}
/// 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: impl Fn() + 'static + Clone + Send,
req: Request<Body>,
) -> impl IntoResponse {
use server_fn::middleware::Service;
let path_string = path.to_string();
let future = move || async move {
let (parts, body) = req.into_parts();
let req = Request::from_parts(parts.clone(), body);
if let Some(mut service) =
server_fn::axum::get_server_fn_service(&path_string)
{
let server_context = DioxusServerContext::new(Arc::new(tokio::sync::RwLock::new(parts)));
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
let mut res = service.run(req).await;
// it it accepts text/html (i.e., is a plain form post) and doesn't already have a
// Location set, then redirect to 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
let mut res_options = server_context.response_parts_mut().unwrap();
res.headers_mut().extend(res_options.headers.drain());
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")
};
#[cfg(target_arch = "wasm32")]
{
use futures_util::future::FutureExt;
let result = tokio::task::spawn_local(future);
let result = result.then(|f| async move { f.unwrap() });
result.await.unwrap_or_else(|e| {
use server_fn::error::NoCustomError;
use server_fn::error::ServerFnErrorSerde;
(
StatusCode::INTERNAL_SERVER_ERROR,
ServerFnError::<NoCustomError>::ServerError(e.to_string())
.ser()
.unwrap_or_default(),
)
.into_response()
})
}
#[cfg(not(target_arch = "wasm32"))]
{
future().await
}
}
+14 -57
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,14 +121,15 @@ 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 crate::axum_adapter::{render_handler, DioxusRouterExt};
use axum::routing::get;
use tower::ServiceBuilder;
let ssr_state = SSRState::new(&cfg);
let router = axum::Router::new().register_server_fns(server_fn_route);
let router = axum::Router::new().register_server_fns();
#[cfg(not(any(feature = "desktop", feature = "mobile")))]
let router = router
.serve_static_assets(cfg.assets_path.clone())
@@ -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();
}
}
}
+1
View File
@@ -7,6 +7,7 @@ use dioxus_lib::prelude::{Element, VirtualDom};
pub use crate::Config;
/// 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>>,
-86
View File
@@ -1,86 +0,0 @@
use std::pin::Pin;
use tracing_futures::Instrument;
use http::{Request, Response};
/// A layer that wraps a service. This can be used to add additional information to the request, or response on top of some other service
pub trait Layer: Send + Sync + 'static {
/// Wrap a boxed service with this layer
fn layer(&self, inner: BoxedService) -> BoxedService;
}
impl<L> Layer for L
where
L: tower_layer::Layer<BoxedService> + Sync + Send + 'static,
L::Service: Service + Send + 'static,
{
fn layer(&self, inner: BoxedService) -> BoxedService {
BoxedService(Box::new(self.layer(inner)))
}
}
/// 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>,
) -> Pin<
Box<
dyn std::future::Future<
Output = Result<Response<hyper::body::Body>, server_fn::ServerFnError>,
> + Send,
>,
>;
}
impl<S> Service for S
where
S: tower::Service<http::Request<hyper::body::Body>, Response = Response<hyper::body::Body>>,
S::Future: Send + 'static,
S::Error: Into<server_fn::ServerFnError>,
{
fn run(
&mut self,
req: http::Request<hyper::body::Body>,
) -> Pin<
Box<
dyn std::future::Future<
Output = Result<Response<hyper::body::Body>, server_fn::ServerFnError>,
> + Send,
>,
> {
let fut = self.call(req).instrument(tracing::trace_span!(
"service",
"{}",
std::any::type_name::<S>()
));
Box::pin(async move { fut.await.map_err(|err| err.into()) })
}
}
/// 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>;
type Error = server_fn::ServerFnError;
type Future = Pin<
Box<
dyn std::future::Future<
Output = Result<http::Response<hyper::body::Body>, server_fn::ServerFnError>,
> + Send,
>,
>;
fn poll_ready(
&mut self,
_cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), Self::Error>> {
Ok(()).into()
}
fn call(&mut self, req: Request<hyper::body::Body>) -> Self::Future {
self.0.run(req)
}
}
+47 -81
View File
@@ -2,125 +2,91 @@
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
#![deny(missing_docs)]
#![cfg_attr(any(docsrs, feature = "nightly-doc"), feature(doc_cfg))]
#![cfg_attr(docsrs, feature(doc_cfg))]
pub use once_cell;
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(feature = "server")]
pub use adapters::{server_fn_service, ServerFnHandler};
mod axum_adapter;
mod config;
mod hooks;
#[cfg(all(debug_assertions, feature = "hot-reload", feature = "server"))]
mod hot_reload;
pub mod launch;
#[cfg(all(
debug_assertions,
feature = "hot-reload",
feature = "server",
not(target_arch = "wasm32")
))]
mod hot_reload;
pub use config::*;
#[cfg(feature = "server")]
mod layer;
#[cfg(feature = "server")]
mod render;
#[cfg(feature = "server")]
mod serve_config;
#[cfg(feature = "server")]
mod server_context;
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")))]
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;
pub use hooks::{server_cached::server_cached, server_future::use_server_future};
#[cfg(feature = "axum")]
#[cfg_attr(docsrs, doc(cfg(feature = "axum")))]
pub use crate::axum_adapter::*;
#[cfg(not(feature = "server"))]
#[cfg_attr(
any(docsrs, feature = "nightly-doc"),
doc(cfg(not(feature = "server")))
)]
#[cfg_attr(docsrs, doc(cfg(not(feature = "server"))))]
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")))]
pub use crate::layer::{Layer, Service};
#[cfg(all(feature = "server", feature = "router"))]
#[cfg_attr(
any(docsrs, feature = "nightly-doc"),
doc(cfg(all(feature = "server", feature = "router")))
)]
#[cfg_attr(docsrs, doc(cfg(all(feature = "server", feature = "router"))))]
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(
any(docsrs, feature = "nightly-doc"),
doc(cfg(all(feature = "server", feature = "axum")))
)]
#[cfg_attr(docsrs, doc(cfg(all(feature = "server", feature = "axum"))))]
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")))]
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")))]
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};
#[cfg(feature = "server")]
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
pub use dioxus_ssr::incremental::IncrementalRendererConfig;
pub use dioxus_server_macro::*;
pub use server_fn::{self, ServerFn as _, ServerFnError};
}
// 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.");
+18 -22
View File
@@ -6,11 +6,10 @@ use dioxus_ssr::{
incremental::{IncrementalRendererConfig, RenderFreshness, WrapBody},
Renderer,
};
use serde::Serialize;
use std::future::Future;
use std::sync::Arc;
use std::sync::RwLock;
use tokio::task::{spawn_blocking, JoinHandle};
use tokio::task::JoinHandle;
use crate::prelude::*;
use dioxus_lib::prelude::*;
@@ -22,7 +21,7 @@ where
{
#[cfg(not(target_arch = "wasm32"))]
{
spawn_blocking(move || {
tokio::task::spawn_blocking(move || {
tokio::runtime::Runtime::new()
.expect("couldn't spawn runtime")
.block_on(f())
@@ -48,7 +47,6 @@ impl SsrRendererPool {
server_context: &DioxusServerContext,
) -> Result<(RenderFreshness, String), dioxus_ssr::incremental::IncrementalRendererError> {
let wrapper = FullstackRenderer {
serialized_props: None,
cfg: cfg.clone(),
server_context: server_context.clone(),
};
@@ -66,7 +64,7 @@ impl SsrRendererPool {
let prev_context = SERVER_CONTEXT.with(|ctx| ctx.replace(server_context));
// poll the future, which may call server_context()
tracing::info!("Rebuilding vdom");
let _ = vdom.rebuild(&mut NoOpMutations);
vdom.rebuild(&mut NoOpMutations);
vdom.wait_for_suspense().await;
tracing::info!("Suspense resolved");
// after polling the future, we need to restore the context
@@ -91,7 +89,11 @@ impl SsrRendererPool {
let _ = tx.send(Ok((renderer, RenderFreshness::now(None), html)));
}
Err(err) => {
dioxus_ssr::incremental::IncrementalRendererError::Other(Box::new(err));
_ = tx.send(Err(
dioxus_ssr::incremental::IncrementalRendererError::Other(Box::new(
err,
)),
));
}
}
});
@@ -122,7 +124,7 @@ impl SsrRendererPool {
.with(|ctx| ctx.replace(Box::new(server_context)));
// poll the future, which may call server_context()
tracing::info!("Rebuilding vdom");
let _ = vdom.rebuild(&mut NoOpMutations);
vdom.rebuild(&mut NoOpMutations);
vdom.wait_for_suspense().await;
tracing::info!("Suspense resolved");
// after polling the future, we need to restore the context
@@ -192,31 +194,25 @@ impl SSRState {
}
/// Render the application to HTML.
pub fn render<'a>(
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,
) -> impl std::future::Future<
Output = Result<RenderResponse, dioxus_ssr::incremental::IncrementalRendererError>,
> + Send
+ 'a {
async move {
let ServeConfig { .. } = cfg;
) -> Result<RenderResponse, dioxus_ssr::incremental::IncrementalRendererError> {
let ServeConfig { .. } = cfg;
let (freshness, html) = self
.renderers
.render_to(cfg, route, virtual_dom_factory, server_context)
.await?;
let (freshness, html) = self
.renderers
.render_to(cfg, route, virtual_dom_factory, server_context)
.await?;
Ok(RenderResponse { html, freshness })
}
Ok(RenderResponse { html, freshness })
}
}
struct FullstackRenderer {
serialized_props: Option<String>,
cfg: ServeConfig,
server_context: DioxusServerContext,
}
@@ -330,7 +326,7 @@ impl RenderResponse {
fn pre_renderer() -> Renderer {
let mut renderer = Renderer::default();
renderer.pre_render = true;
renderer.into()
renderer
}
fn incremental_pre_renderer(
+3 -6
View File
@@ -1,16 +1,12 @@
#![allow(non_snake_case)]
//! Configeration for how to serve a Dioxus application
//! Configuration for how to serve a Dioxus application
#[cfg(feature = "router")]
use crate::router::*;
use std::fs::File;
use std::io::Read;
use std::path::PathBuf;
use dioxus_lib::prelude::*;
/// 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)]
#[derive(Clone, Default)]
pub struct ServeConfigBuilder {
pub(crate) root_id: Option<&'static str>,
pub(crate) index_html: Option<String>,
@@ -149,6 +145,7 @@ pub(crate) struct IndexHtml {
#[derive(Clone)]
pub struct ServeConfig {
pub(crate) index: IndexHtml,
#[allow(dead_code)]
pub(crate) assets_path: PathBuf,
pub(crate) incremental:
Option<std::sync::Arc<dioxus_ssr::incremental::IncrementalRendererConfig>>,
+16 -19
View File
@@ -1,5 +1,4 @@
use crate::html_storage::HTMLData;
pub use server_fn_impl::*;
use std::sync::Arc;
use std::sync::RwLock;
@@ -13,7 +12,7 @@ pub struct DioxusServerContext {
std::sync::RwLock<anymap::Map<dyn anymap::any::Any + Send + Sync + 'static>>,
>,
response_parts: std::sync::Arc<std::sync::RwLock<http::response::Parts>>,
pub(crate) parts: Arc<RwLock<http::request::Parts>>,
pub(crate) parts: Arc<tokio::sync::RwLock<http::request::Parts>>,
html_data: Arc<RwLock<HTMLData>>,
}
@@ -25,7 +24,9 @@ impl Default for DioxusServerContext {
response_parts: std::sync::Arc::new(RwLock::new(
http::response::Response::new(()).into_parts().0,
)),
parts: std::sync::Arc::new(RwLock::new(http::request::Request::new(()).into_parts().0)),
parts: std::sync::Arc::new(tokio::sync::RwLock::new(
http::request::Request::new(()).into_parts().0,
)),
html_data: Arc::new(RwLock::new(HTMLData::default())),
}
}
@@ -41,7 +42,7 @@ mod server_fn_impl {
impl DioxusServerContext {
/// Create a new server context from a request
pub fn new(parts: impl Into<Arc<RwLock<http::request::Parts>>>) -> Self {
pub fn new(parts: impl Into<Arc<tokio::sync::RwLock<http::request::Parts>>>) -> Self {
Self {
parts: parts.into(),
shared_context: Arc::new(RwLock::new(SendSyncAnyMap::new())),
@@ -83,19 +84,15 @@ mod server_fn_impl {
/// Get the request that triggered:
/// - The initial SSR render if called from a ScopeState or ServerFn
/// - The server function to be called if called from a server function after the initial render
pub fn request_parts(
&self,
) -> std::sync::LockResult<RwLockReadGuard<'_, http::request::Parts>> {
self.parts.read()
pub fn request_parts(&self) -> tokio::sync::RwLockReadGuard<'_, http::request::Parts> {
self.parts.blocking_read()
}
/// Get the request that triggered:
/// - The initial SSR render if called from a ScopeState or ServerFn
/// - The server function to be called if called from a server function after the initial render
pub fn request_parts_mut(
&self,
) -> std::sync::LockResult<RwLockWriteGuard<'_, http::request::Parts>> {
self.parts.write()
pub fn request_parts_mut(&self) -> tokio::sync::RwLockWriteGuard<'_, http::request::Parts> {
self.parts.blocking_write()
}
/// Extract some part from the request
@@ -123,7 +120,7 @@ mod server_fn_impl {
}
std::thread_local! {
pub(crate) static SERVER_CONTEXT: std::cell::RefCell<Box<DioxusServerContext>> = std::cell::RefCell::new(Box::new(DioxusServerContext::default() ));
pub(crate) static SERVER_CONTEXT: std::cell::RefCell<Box<DioxusServerContext>> = Default::default();
}
/// Get information about the current server request.
@@ -186,7 +183,7 @@ impl<F: std::future::Future> std::future::Future for ProvideServerContext<F> {
}
/// A trait for extracting types from the server context
#[async_trait::async_trait(?Send)]
#[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: std::error::Error;
@@ -216,24 +213,24 @@ impl<T: 'static> std::error::Error for NotFoundInServerContext<T> {}
pub struct FromContext<T: std::marker::Send + std::marker::Sync + Clone + 'static>(pub(crate) T);
#[async_trait::async_trait(?Send)]
#[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.clone().get::<T>().ok_or_else(|| {
Ok(Self(req.clone().get::<T>().ok_or({
NotFoundInServerContext::<T>(std::marker::PhantomData::<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;
#[cfg(feature = "axum")]
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl<
I: axum::extract::FromRequestParts<(), Rejection = R>,
R: axum::response::IntoResponse + std::error::Error,
@@ -242,6 +239,6 @@ impl<
type Rejection = R;
async fn from_request(req: &DioxusServerContext) -> Result<Self, Self::Rejection> {
Ok(I::from_request_parts(&mut *req.request_parts_mut().unwrap(), &()).await?)
Ok(I::from_request_parts(&mut req.request_parts_mut(), &()).await?)
}
}
@@ -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 {
@@ -171,23 +171,3 @@ pub enum ServerRegistrationFnError {
#[error("The server function registry is poisoned: {0}")]
Poisoned(String),
}
/// Defines a "server function." A server function can be called from the server or the client,
/// but the body of its code will only be run on the server, i.e., if a crate feature `ssr` is enabled.
///
/// Server functions are created using the `server` macro.
///
/// The set of server functions
/// can be queried on the server for routing purposes by calling [server_fn::ServerFunctionRegistry::get].
///
/// Technically, the trait is implemented on a type that describes the server function's arguments, not the function itself.
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")))]
fn register_explicit() -> Result<(), server_fn::ServerFnError> {
Self::register_in_explicit::<DioxusServerFnRegistry>()
}
}
impl<T> DioxusServerFn for T where T: server_fn::ServerFn<()> {}
+2 -4
View File
@@ -1,19 +1,17 @@
use std::{
io::{BufRead, BufReader, Write},
io::Write,
path::PathBuf,
str::FromStr,
sync::{Arc, Mutex},
};
use crate::HotReloadMsg;
use dioxus_core::Template;
use dioxus_rsx::{
hot_reload::{FileMap, FileMapBuildResult, UpdateResult},
HotReloadingContext,
};
use interprocess_docfix::local_socket::{LocalSocketListener, LocalSocketStream};
use interprocess_docfix::local_socket::LocalSocketListener;
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
use serde::{Deserialize, Serialize};
#[cfg(feature = "file_watcher")]
use dioxus_html::HtmlCtx;
-5
View File
@@ -1,5 +0,0 @@
{
"rust-analyzer.cargo.features": [
"warp"
]
}
+4 -38
View File
@@ -31,42 +31,20 @@ dioxus-hot-reload = { workspace = true, optional = true }
dioxus-cli-config = { workspace = true }
generational-box = { workspace = true }
# warp
warp = { version = "0.3.3", optional = true }
# axum
axum = { version = "0.6.1", optional = true, features = ["ws"] }
# salvo
salvo = { version = "0.63.0", optional = true, features = ["websocket", "affix"] }
once_cell = "1.17.1"
async-trait = "0.1.71"
# rocket
rocket = { version = "0.5.0", optional = true }
rocket_ws = { version = "0.1.0", optional = true }
# actix is ... complicated?
# actix-files = { version = "0.6.2", optional = true }
# actix-web = { version = "4.2.1", optional = true }
# actix-ws = { version = "0.2.5", optional = true }
axum = { workspace = true, optional = true, features = ["ws"] }
[dev-dependencies]
pretty_env_logger = { version = "0.5.0" }
tokio = { workspace = true, features = ["full"] }
warp = "0.3.3"
axum = { version = "0.6.1", features = ["ws"] }
salvo = { version = "0.63.0", features = ["affix", "websocket"] }
rocket = "0.5.0"
rocket_ws = "0.1.0"
tower = "0.4.13"
axum = { workspace = true, features = ["ws"] }
tower = { workspace = true }
dioxus = { workspace = true }
[features]
default = ["hot-reload"]
# actix = ["actix-files", "actix-web", "actix-ws"]
axum = ["dep:axum"]
hot-reload = ["dioxus-hot-reload"]
rocket = ["dep:rocket", "dep:rocket_ws"]
[[example]]
name = "axum"
@@ -75,15 +53,3 @@ required-features = ["axum"]
[[example]]
name = "axum_stress"
required-features = ["axum"]
[[example]]
name = "salvo"
required-features = ["salvo"]
[[example]]
name = "warp"
required-features = ["warp"]
[[example]]
name = "rocket"
required-features = ["rocket"]
-3
View File
@@ -26,9 +26,6 @@
The current backend frameworks supported include:
- Axum
- Warp
- Salvo
- Rocket
Dioxus-LiveView exports some primitives to wire up an app into an existing backend framework.
+2 -2
View File
@@ -23,8 +23,8 @@ async fn main() {
println!("Listening on http://{addr}");
axum::Server::bind(&addr.to_string().parse().unwrap())
.serve(app.into_make_service())
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}
+2 -2
View File
@@ -55,8 +55,8 @@ async fn main() {
println!("Listening on http://{addr}");
axum::Server::bind(&addr.to_string().parse().unwrap())
.serve(app.into_make_service())
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}
-76
View File
@@ -1,76 +0,0 @@
#[macro_use]
extern crate rocket;
use dioxus::prelude::*;
use dioxus_liveview::LiveViewPool;
use rocket::response::content::RawHtml;
use rocket::{Config, Rocket, State};
use rocket_ws::{Channel, WebSocket};
fn app() -> Element {
let mut num = use_signal(|| 0);
rsx! {
div {
"hello Rocket! {num}"
button { onclick: move |_| num += 1, "Increment" }
}
}
}
fn index_page_with_glue(glue: &str) -> RawHtml<String> {
RawHtml(format!(
r#"
<!DOCTYPE html>
<html>
<head> <title>Dioxus LiveView with Rocket</title> </head>
<body> <div id="main"></div> </body>
{glue}
</html>
"#,
glue = glue
))
}
#[get("/")]
async fn index(config: &Config) -> RawHtml<String> {
index_page_with_glue(&dioxus_liveview::interpreter_glue(&format!(
"ws://{addr}:{port}/ws",
addr = config.address,
port = config.port,
)))
}
#[get("/as-path")]
async fn as_path() -> RawHtml<String> {
index_page_with_glue(&dioxus_liveview::interpreter_glue("/ws"))
}
#[get("/ws")]
fn ws(ws: WebSocket, pool: &State<LiveViewPool>) -> Channel<'static> {
let pool = pool.inner().to_owned();
ws.channel(move |stream| {
Box::pin(async move {
let _ = pool
.launch(dioxus_liveview::rocket_socket(stream), app)
.await;
Ok(())
})
})
}
#[tokio::main]
async fn main() {
let view = dioxus_liveview::LiveViewPool::new();
Rocket::build()
.manage(view)
.mount("/", routes![index, as_path, ws])
.ignite()
.await
.expect("Failed to ignite rocket")
.launch()
.await
.expect("Failed to launch rocket");
}
-67
View File
@@ -1,67 +0,0 @@
use dioxus::prelude::*;
use dioxus_liveview::LiveViewPool;
use salvo::affix;
use salvo::prelude::*;
use std::net::SocketAddr;
use std::sync::Arc;
fn app() -> Element {
let mut num = use_signal(|| 0);
rsx! {
div {
"hello salvo! {num}"
button { onclick: move |_| num += 1, "Increment" }
}
}
}
#[tokio::main]
async fn main() {
pretty_env_logger::init();
let addr = "127.0.0.1:3030";
let acceptor = TcpListener::new(addr).bind().await;
let view = LiveViewPool::new();
let router = Router::new()
.hoop(affix::inject(Arc::new(view)))
.get(index)
.push(Router::with_path("ws").get(connect));
println!("Listening on http://{}", addr);
Server::new(acceptor).serve(router).await;
}
#[handler]
fn index(res: &mut Response) {
let addr: SocketAddr = ([127, 0, 0, 1], 3030).into();
res.render(Text::Html(format!(
r#"
<!DOCTYPE html>
<html>
<head> <title>Dioxus LiveView with Salvo</title> </head>
<body> <div id="main"></div> </body>
{glue}
</html>
"#,
glue = dioxus_liveview::interpreter_glue(&format!("ws://{addr}/ws"))
)));
}
#[handler]
async fn connect(
req: &mut Request,
depot: &mut Depot,
res: &mut Response,
) -> Result<(), StatusError> {
let view = depot.obtain::<Arc<LiveViewPool>>().unwrap().clone();
WebSocketUpgrade::new()
.upgrade(req, res, |ws| async move {
_ = view.launch(dioxus_liveview::salvo_socket(ws), app).await;
})
.await
}
-56
View File
@@ -1,56 +0,0 @@
use dioxus::prelude::*;
use dioxus_liveview::warp_adapter::warp_socket;
use dioxus_liveview::LiveViewPool;
use std::net::SocketAddr;
use warp::ws::Ws;
use warp::Filter;
fn app() -> Element {
let mut num = use_signal(|| 0);
rsx! {
div {
"hello warp! {num}"
button {
onclick: move |_| num += 1,
"Increment"
}
}
}
}
#[tokio::main]
async fn main() {
pretty_env_logger::init();
let addr: SocketAddr = ([127, 0, 0, 1], 3030).into();
let index = warp::path::end().map(move || {
warp::reply::html(format!(
r#"
<!DOCTYPE html>
<html>
<head> <title>Dioxus LiveView with Warp</title> </head>
<body> <div id="main"></div> </body>
{glue}
</html>
"#,
glue = dioxus_liveview::interpreter_glue(&format!("ws://{addr}/ws/"))
))
});
let pool = LiveViewPool::new();
let ws = warp::path("ws")
.and(warp::ws())
.and(warp::any().map(move || pool.clone()))
.map(move |ws: Ws, pool: LiveViewPool| {
ws.on_upgrade(|ws| async move {
let _ = pool.launch(warp_socket(ws), app).await;
})
});
println!("Listening on http://{}", addr);
warp::serve(index.or(ws)).run(addr).await;
}
@@ -81,10 +81,8 @@ impl LiveviewRouter for Router {
}
async fn start(self, address: impl Into<std::net::SocketAddr>) {
if let Err(err) = axum::Server::bind(&address.into())
.serve(self.into_make_service())
.await
{
let listener = tokio::net::TcpListener::bind(address.into()).await.unwrap();
if let Err(err) = axum::serve(listener, self.into_make_service()).await {
eprintln!("Failed to start axum server: {}", err);
}
}
-15
View File
@@ -2,26 +2,11 @@ use std::future::Future;
use dioxus_core::{Element, VirtualDom};
#[cfg(feature = "warp")]
pub mod warp_adapter;
#[cfg(feature = "warp")]
pub use warp_adapter::*;
#[cfg(feature = "axum")]
pub mod axum_adapter;
#[cfg(feature = "axum")]
pub use axum_adapter::*;
#[cfg(feature = "salvo")]
pub mod salvo_adapter;
#[cfg(feature = "salvo")]
pub use salvo_adapter::*;
#[cfg(feature = "rocket")]
pub mod rocket_adapter;
#[cfg(feature = "rocket")]
pub use rocket_adapter::*;
/// A trait for servers that can be used to host a LiveView app.
pub trait LiveviewRouter {
/// Create a new router.
@@ -1,113 +0,0 @@
use crate::LiveViewPool;
use crate::LiveviewRouter;
use crate::{LiveViewError, LiveViewSocket};
use rocket::futures::{SinkExt, StreamExt};
use rocket::response::content::RawHtml;
use rocket::{get, routes, State};
use rocket_ws::Channel;
use rocket_ws::WebSocket;
use rocket_ws::{result::Error, stream::DuplexStream, Message};
use std::sync::Arc;
/// Convert a Rocket WebSocket into a `LiveViewSocket`.
///
/// This is required to launch a LiveView app using the Rocket web framework.
pub fn rocket_socket(stream: DuplexStream) -> impl LiveViewSocket {
stream
.map(transform_rx)
.with(transform_tx)
.sink_map_err(|_| LiveViewError::SendingFailed)
}
fn transform_rx(message: Result<Message, Error>) -> Result<Vec<u8>, LiveViewError> {
message
.map_err(|_| LiveViewError::SendingFailed)?
.into_text()
.map(|s| s.into_bytes())
.map_err(|_| LiveViewError::SendingFailed)
}
async fn transform_tx(message: Vec<u8>) -> Result<Message, Error> {
Ok(Message::Binary(message))
}
impl LiveviewRouter for rocket::Rocket<rocket::Build> {
fn create_default_liveview_router() -> Self {
Self::build()
}
fn with_virtual_dom(
self,
route: &str,
app: impl Fn() -> dioxus_core::prelude::VirtualDom + Send + Sync + 'static,
) -> Self {
#[get("/")]
async fn index(request: &rocket::route::Route) -> RawHtml<String> {
let route = request.uri.base();
let glue = crate::interpreter_glue(&format!("{route}/ws",));
let title = crate::app_title();
RawHtml(format!(
r#"
<!DOCTYPE html>
<html>
<head> <title>{title}</title> </head>
<body> <div id="main"></div> </body>
{glue}
</html>
"#
))
}
#[get("/ws")]
fn ws(ws: WebSocket, app: &State<LiveviewApp>) -> Channel<'static> {
let app = app.inner();
let pool = app.pool.clone();
let app = app.app.clone();
ws.channel(move |stream| {
Box::pin(async move {
let _ = pool
.launch_virtualdom(crate::rocket_socket(stream), move || app())
.await;
Ok(())
})
})
}
struct LiveviewApp {
app: Arc<dyn Fn() -> dioxus_core::prelude::VirtualDom + Send + Sync + 'static>,
pool: LiveViewPool,
}
let app = Arc::new(app);
let view = crate::LiveViewPool::new();
self.manage(LiveviewApp {
app: app,
pool: view,
})
.mount(route, routes![index, ws])
}
async fn start(self, address: impl Into<std::net::SocketAddr>) {
let address = address.into();
let figment = self
.figment()
.clone()
.merge((rocket::Config::PORT, address.port()))
.merge((rocket::Config::ADDRESS, address.ip()));
self.configure(figment)
.ignite()
.await
.expect("Failed to ignite rocket")
.launch()
.await
.expect("Failed to launch rocket");
}
}
@@ -1,108 +0,0 @@
use crate::LiveViewPool;
use crate::LiveviewRouter;
use crate::{LiveViewError, LiveViewSocket};
use futures_util::{SinkExt, StreamExt};
use salvo::conn::TcpListener;
use salvo::http::StatusError;
use salvo::websocket::WebSocketUpgrade;
use salvo::websocket::{Message, WebSocket};
use salvo::writing::Text;
use salvo::Listener;
use salvo::Server;
use salvo::{handler, Depot, Request, Response, Router};
use std::sync::Arc;
/// Convert a Salvo WebSocket into a `LiveViewSocket`.
///
/// This is required to launch a LiveView app using the Salvo web framework.
pub fn salvo_socket(ws: WebSocket) -> impl LiveViewSocket {
ws.map(transform_rx)
.with(transform_tx)
.sink_map_err(|_| LiveViewError::SendingFailed)
}
fn transform_rx(message: Result<Message, salvo::Error>) -> Result<Vec<u8>, LiveViewError> {
let as_bytes = message.map_err(|_| LiveViewError::SendingFailed)?;
Ok(as_bytes.into())
}
async fn transform_tx(message: Vec<u8>) -> Result<Message, salvo::Error> {
Ok(Message::binary(message))
}
#[derive(Clone)]
struct LiveviewApp {
app: Arc<dyn Fn() -> dioxus_core::prelude::VirtualDom + Send + Sync + 'static>,
pool: Arc<LiveViewPool>,
}
impl LiveviewRouter for Router {
fn create_default_liveview_router() -> Self {
Self::new()
}
fn with_virtual_dom(
self,
route: &str,
app: impl Fn() -> dioxus_core::prelude::VirtualDom + Send + Sync + 'static,
) -> Self {
let app = Arc::new(app);
let view = crate::LiveViewPool::new();
self.push(
Router::with_path(route)
.hoop(salvo::affix::inject(LiveviewApp {
app: app,
pool: Arc::new(view),
}))
.get(index)
.push(Router::with_path("ws").get(connect)),
)
}
async fn start(self, address: impl Into<std::net::SocketAddr>) {
let address = address.into();
let acceptor = TcpListener::new(address).bind().await;
Server::new(acceptor).serve(self).await;
}
}
#[handler]
fn index(req: &mut Request, res: &mut Response) {
let base = req.uri().path();
let title = crate::app_title();
res.render(Text::Html(format!(
r#"
<!DOCTYPE html>
<html>
<head> <title>{title}</title> </head>
<body> <div id="main"></div> </body>
{glue}
</html>
"#,
glue = crate::interpreter_glue(&format!("{base}/ws"))
)));
}
#[handler]
async fn connect(
req: &mut Request,
depot: &mut Depot,
res: &mut Response,
) -> Result<(), StatusError> {
let app = depot.obtain::<LiveviewApp>().unwrap().clone();
let view = app.pool.clone();
let app = app.app.clone();
WebSocketUpgrade::new()
.upgrade(req, res, |ws| async move {
_ = view
.launch_virtualdom(crate::salvo_socket(ws), move || app())
.await;
})
.await
}
@@ -1,25 +0,0 @@
use crate::{LiveViewError, LiveViewSocket};
use futures_util::{SinkExt, StreamExt};
use warp::ws::{Message, WebSocket};
/// Convert a warp WebSocket into a `LiveViewSocket`.
///
/// This is required to launch a LiveView app using the warp web framework.
pub fn warp_socket(ws: WebSocket) -> impl LiveViewSocket {
ws.map(transform_rx)
.with(transform_tx)
.sink_map_err(|_| LiveViewError::SendingFailed)
}
fn transform_rx(message: Result<Message, warp::Error>) -> Result<Vec<u8>, LiveViewError> {
// Destructure the `message` into the buffer we got from warp.
let msg = message
.map_err(|_| LiveViewError::SendingFailed)?
.into_bytes();
Ok(msg)
}
async fn transform_tx(message: Vec<u8>) -> Result<Message, warp::Error> {
Ok(Message::binary(message))
}
-5
View File
@@ -1,12 +1,7 @@
use dioxus_core::*;
use std::any::Any;
#[cfg(feature = "axum")]
pub type Config = crate::Config<axum::Router>;
#[cfg(all(feature = "salvo", not(feature = "axum")))]
pub type Config = crate::Config<salvo::Router>;
#[cfg(all(feature = "rocket", not(any(feature = "axum", feature = "salvo"))))]
pub type Config = crate::Config<rocket::Rocket<rocket::Build>>;
/// Launches the WebView and runs the event loop, with configuration and root props.
pub fn launch(
+4 -3
View File
@@ -15,6 +15,7 @@ mod config;
mod eval;
mod events;
pub use config::*;
#[cfg(feature = "axum")]
pub mod launch;
pub trait WebsocketTx: SinkExt<String, Error = LiveViewError> {}
@@ -25,7 +26,7 @@ impl<T> WebsocketRx for T where T: StreamExt<Item = Result<String, LiveViewError
#[derive(Debug, thiserror::Error)]
pub enum LiveViewError {
#[error("warp error")]
#[error("Sending to client error")]
SendingFailed,
}
@@ -113,7 +114,7 @@ pub fn interpreter_glue(url_or_path: &str) -> String {
// If the url starts with a `/`, generate glue which reuses current host
let get_ws_url = if url_or_path.starts_with('/') {
r#"
let loc = window.location;
let loc = window.location;
let new_url = "";
if (loc.protocol === "https:") {{
new_url = "wss:";
@@ -135,7 +136,7 @@ pub fn interpreter_glue(url_or_path: &str) -> String {
function __dioxusGetWsUrl(path) {{
{get_ws_url}
}}
var WS_ADDR = __dioxusGetWsUrl("{url_or_path}");
{handle_edits}
</script>
@@ -10,4 +10,4 @@ publish = false
dioxus = { workspace = true }
dioxus-liveview = { workspace = true, features = ["axum"] }
tokio = { version = "1.19.2", features = ["full"] }
axum = { version = "0.6.1", features = ["ws"] }
axum = { workspace = true, features = ["ws"] }
@@ -57,8 +57,8 @@ async fn main() {
println!("Listening on http://{addr}");
axum::Server::bind(&addr.to_string().parse().unwrap())
.serve(app.into_make_service())
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}
+1 -1
View File
@@ -39,7 +39,7 @@ web = ["gloo", "web-sys", "wasm-bindgen", "gloo-utils", "js-sys"]
fullstack = ["dioxus-fullstack"]
[dev-dependencies]
axum = { version = "0.6.1", features = ["ws"] }
axum = { workspace = true, features = ["ws"] }
dioxus = { workspace = true, features = ["router" ]}
# dioxus-liveview = { workspace = true, features = ["axum"] }
dioxus-ssr = { path = "../ssr" }
+69 -62
View File
@@ -11,7 +11,45 @@ use dioxus_ssr::Renderer;
pub fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("build 1000 routes", |b| {
let mut renderer = IncrementalRenderer::builder(DefaultRenderer {
let mut renderer = IncrementalRenderer::builder()
.static_dir("./static")
.invalidate_after(Duration::from_secs(10))
.build();
let wrapper = DefaultRenderer {
before_body: r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,
initial-scale=1.0">
<title>Dioxus Application</title>
</head>
<body>"#
.to_string(),
after_body: r#"</body>
</html>"#
.to_string(),
};
b.iter(|| {
tokio::runtime::Runtime::new().unwrap().block_on(async {
for id in 0..1000 {
render_route(
&mut renderer,
Route::Post { id },
&mut tokio::io::sink(),
|_| Box::pin(async move {}),
&wrapper,
)
.await
.unwrap();
}
})
})
});
c.bench_function("build 1000 routes no memory cache", |b| {
let wrapper = DefaultRenderer {
before_body: r#"<!DOCTYPE html>
<html lang="en">
<head>
@@ -25,54 +63,22 @@ pub fn criterion_benchmark(c: &mut Criterion) {
after_body: r#"</body>
</html>"#
.to_string(),
})
.static_dir("./static")
.invalidate_after(Duration::from_secs(10))
.build();
b.iter(|| {
tokio::runtime::Runtime::new().unwrap().block_on(async {
for id in 0..1000 {
render_route(
&mut renderer,
Route::Post { id },
&mut tokio::io::sink(),
|_| {},
)
.await
.unwrap();
}
})
})
});
c.bench_function("build 1000 routes no memory cache", |b| {
};
b.to_async(tokio::runtime::Runtime::new().unwrap())
.iter(|| async {
let mut renderer = IncrementalRenderer::builder(DefaultRenderer {
before_body: r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,
initial-scale=1.0">
<title>Dioxus Application</title>
</head>
<body>"#
.to_string(),
after_body: r#"</body>
</html>"#
.to_string(),
})
.static_dir("./static")
.memory_cache_limit(0)
.invalidate_after(Duration::from_secs(10))
.build();
let mut renderer = IncrementalRenderer::builder()
.static_dir("./static")
.memory_cache_limit(0)
.invalidate_after(Duration::from_secs(10))
.build();
for id in 0..1000 {
render_route(
&mut renderer,
Route::Post { id },
&mut tokio::io::sink(),
|_| {},
|_| Box::pin(async move {}),
&wrapper,
)
.await
.unwrap();
@@ -89,7 +95,7 @@ pub fn criterion_benchmark(c: &mut Criterion) {
RenderPathProps::builder().path(Route::Post { id }).build(),
);
vdom.rebuild();
vdom.rebuild_in_place();
struct Ignore;
@@ -104,27 +110,28 @@ pub fn criterion_benchmark(c: &mut Criterion) {
})
});
c.bench_function("cache static", |b| {
let wrapper = DefaultRenderer {
before_body: r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,
initial-scale=1.0">
<title>Dioxus Application</title>
</head>
<body>"#
.to_string(),
after_body: r#"</body>
</html>"#
.to_string(),
};
b.to_async(tokio::runtime::Runtime::new().unwrap())
.iter(|| async {
let mut renderer = IncrementalRenderer::builder(DefaultRenderer {
before_body: r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,
initial-scale=1.0">
<title>Dioxus Application</title>
</head>
<body>"#
.to_string(),
after_body: r#"</body>
</html>"#
.to_string(),
})
.static_dir("./static")
.build();
let mut renderer = IncrementalRenderer::builder()
.static_dir("./static")
.build();
pre_cache_static_routes::<Route, _>(&mut renderer)
pre_cache_static_routes::<Route, _>(&mut renderer, &wrapper)
.await
.unwrap();
})
@@ -146,7 +153,7 @@ fn Blog() -> Element {
#[component]
fn Post(id: usize) -> Element {
rsx! {
for _ in 0..*id {
for _ in 0..id {
div {
"PostId: {id}"
}
+13 -10
View File
@@ -26,28 +26,31 @@ async fn main() {
"/ws",
get(move |ws: WebSocketUpgrade| async move {
ws.on_upgrade(move |socket| async move {
_ = view
.launch(dioxus_liveview::axum_socket(socket), Route::Home {})
.await;
_ = view.launch(dioxus_liveview::axum_socket(socket), app).await;
})
}),
);
println!("Listening on http://{listen_address}");
axum::Server::bind(&listen_address.to_string().parse().unwrap())
.serve(app.into_make_service())
let listener = tokio::net::TcpListener::bind(&listen_address)
.await
.unwrap();
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}
#[cfg(not(feature = "liveview"))]
fn main() {
launch(|| {
rsx! {
Router::<Route> {}
}
})
launch(app)
}
fn app() -> Element {
rsx! {
Router::<Route> {}
}
}
#[component]
+10 -11
View File
@@ -166,8 +166,8 @@ where
///
/// Panics if the function is not called in a dioxus runtime with a Liveview context.
pub fn new_with_initial_path(initial_path: R) -> Self {
let (action_tx, action_rx) = tokio::sync::mpsc::unbounded_channel::<Action<R>>();
let action_rx = Arc::new(Mutex::new(action_rx));
let (action_tx, mut action_rx) = tokio::sync::mpsc::unbounded_channel::<Action<R>>();
let timeline = Arc::new(Mutex::new(Timeline::new(initial_path)));
let updater_callback: Arc<RwLock<Arc<dyn Fn() + Send + Sync>>> =
Arc::new(RwLock::new(Arc::new(|| {})));
@@ -175,20 +175,17 @@ where
let eval_provider = consume_context::<Rc<dyn EvalProvider>>();
let create_eval = Rc::new(move |script: &str| {
eval_provider
.new_evaluator(script.to_string())
.map(UseEval::new)
}) as Rc<dyn Fn(&str) -> Result<UseEval, EvalError>>;
UseEval::new(eval_provider.new_evaluator(script.to_string()))
}) as Rc<dyn Fn(&str) -> UseEval>;
// Listen to server actions
spawn({
let timeline = timeline.clone();
let action_rx = action_rx.clone();
let create_eval = create_eval.clone();
async move {
let mut action_rx = action_rx.lock().expect("unpoisoned mutex");
loop {
let eval = action_rx.recv().await.expect("sender to exist");
let _ = match eval {
Action::GoBack => create_eval(
r#"
@@ -256,7 +253,7 @@ where
history.length,
];
"#,
).expect("failed to load state").await.expect("serializable state");
).await.expect("serializable state");
let (route, state, session, depth) = serde_json::from_value::<(
String,
Option<State>,
@@ -276,7 +273,8 @@ where
// Call the updater callback
(updater.read().unwrap())();
create_eval(&format!(r#"
create_eval(&format!(
r#"
// this does not trigger a PopState event
history.replaceState({state}, "", "{route}");
sessionStorage.setItem("liveview", '{session}');
@@ -287,7 +285,8 @@ where
event.state,
]);
}});
"#)).expect("failed to initialize popstate")
"#
))
};
loop {
-16
View File
@@ -289,17 +289,9 @@ pub trait HistoryProvider<R: Routable> {
pub(crate) trait AnyHistoryProvider {
fn parse_route(&self, route: &str) -> Result<Rc<dyn Any>, String>;
#[must_use]
fn accepts_type_id(&self, type_id: &std::any::TypeId) -> bool;
#[must_use]
fn current_route(&self) -> Rc<dyn Any>;
#[must_use]
fn current_prefix(&self) -> Option<String> {
None
}
#[must_use]
fn can_go_back(&self) -> bool {
true
@@ -359,19 +351,11 @@ where
.map(|route| Rc::new(route) as Rc<dyn Any>)
}
fn accepts_type_id(&self, type_id: &std::any::TypeId) -> bool {
type_id == &std::any::TypeId::of::<R>()
}
fn current_route(&self) -> Rc<dyn Any> {
let route = self.inner.current_route();
Rc::new(route)
}
fn current_prefix(&self) -> Option<String> {
self.inner.current_prefix()
}
fn can_go_back(&self) -> bool {
self.inner.can_go_back()
}
-26
View File
@@ -311,32 +311,6 @@ pub trait Routable: FromStr + Display + Clone + 'static {
}
}
trait RoutableFactory {
type Err: Display;
type Routable: Routable + FromStr<Err = Self::Err>;
}
impl<R: Routable + FromStr> RoutableFactory for R
where
<R as FromStr>::Err: Display,
{
type Err = <R as FromStr>::Err;
type Routable = R;
}
trait RouteRenderable: Display + 'static {
fn render(&self, level: usize) -> Element;
}
impl<R: Routable> RouteRenderable for R
where
<R as FromStr>::Err: Display,
{
fn render(&self, level: usize) -> Element {
self.render(level)
}
}
/// A type erased map of the site structure.
#[derive(Debug, Clone, PartialEq)]
pub struct SiteMapSegment {
+47 -42
View File
@@ -1,11 +1,9 @@
use std::sync::Arc;
use crate::contexts::router::RoutingCallback;
use crate::history::HistoryProvider;
use crate::prelude::*;
use crate::routable::Routable;
use dioxus_lib::prelude::*;
use crate::prelude::*;
use std::sync::Arc;
/// Global configuration options for the router.
///
@@ -31,43 +29,6 @@ pub struct RouterConfig<R: Routable> {
pub(crate) initial_route: Option<R>,
}
macro_rules! default_history {
($initial_route:ident) => {
{
// If we are on wasm32 and the web feature is enabled, use the web history.
#[cfg(all(target_arch = "wasm32", feature = "web"))]
return Box::new(AnyHistoryProviderImplWrapper::new(WebHistory::<R>::default()));
// If we are using dioxus fullstack and the ssr feature is enabled, use the memory history with the initial path set to the current path in fullstack
#[cfg(all(feature = "fullstack", feature = "ssr"))]
return Box::new(AnyHistoryProviderImplWrapper::new(MemoryHistory::<R>::with_initial_path(
dioxus_fullstack::prelude::server_context()
.request_parts()
.unwrap()
.uri
.to_string()
.parse()
.unwrap_or_else(|err| {
tracing::error!("Failed to parse uri: {}", err);
"/"
.parse()
.unwrap_or_else(|err| {
panic!("Failed to parse uri: {}", err);
})
}),
)));
// If we are not on wasm32 and the liveview feature is enabled, use the liveview history.
#[cfg(all(feature = "liveview"))]
return Box::new(AnyHistoryProviderImplWrapper::new(LiveviewHistory::new_with_initial_path($initial_route)));
// Otherwise use the memory history.
#[cfg(all(
not(all(target_arch = "wasm32", feature = "web")),
not(all(feature = "liveview", not(target_arch = "wasm32"))),
))]
Box::new(AnyHistoryProviderImplWrapper::new(MemoryHistory::with_initial_path($initial_route)))
}
};
}
impl<R: Routable + Clone> Default for RouterConfig<R>
where
<R as std::str::FromStr>::Err: std::fmt::Display,
@@ -94,7 +55,7 @@ where
));
self.history
.take()
.unwrap_or_else(|| default_history!(initial_route))
.unwrap_or_else(|| default_history(initial_route))
}
}
@@ -154,3 +115,47 @@ where
}
}
}
/// Get the default history provider for the current platform.
#[allow(unreachable_code, unused)]
fn default_history<R: Routable + Clone>(initial_route: R) -> Box<dyn AnyHistoryProvider>
where
<R as std::str::FromStr>::Err: std::fmt::Display,
{
// If we're on the web and have wasm, use the web history provider
#[cfg(all(target_arch = "wasm32", feature = "web"))]
return Box::new(AnyHistoryProviderImplWrapper::new(
WebHistory::<R>::default(),
));
// If we're using fullstack and server side rendering, use the memory history provider
#[cfg(all(feature = "fullstack", feature = "ssr"))]
return Box::new(AnyHistoryProviderImplWrapper::new(
MemoryHistory::<R>::with_initial_path(
dioxus_fullstack::prelude::server_context()
.request_parts()
.uri
.to_string()
.parse()
.unwrap_or_else(|err| {
tracing::error!("Failed to parse uri: {}", err);
"/".parse().unwrap_or_else(|err| {
panic!("Failed to parse uri: {}", err);
})
}),
),
));
// If liveview is enabled, use the liveview history provider
#[cfg(feature = "liveview")]
return Box::new(AnyHistoryProviderImplWrapper::new(
LiveviewHistory::new_with_initial_path(initial_route),
));
// If none of the above, use the memory history provider, which is a decent enough fallback
// Eventually we want to integrate with the mobile history provider, and other platform providers
Box::new(AnyHistoryProviderImplWrapper::new(
MemoryHistory::with_initial_path(initial_route),
))
}
+2 -2
View File
@@ -17,11 +17,11 @@ proc-macro2 = "^1.0.63"
quote = "^1.0.26"
syn = { version = "2", features = ["full"] }
convert_case = "^0.6.0"
server_fn_macro = "^0.5.2"
server_fn_macro = "^0.6.5"
[lib]
proc-macro = true
[features]
default = []
axum = ["server_fn_macro/axum"]
server = ["server_fn_macro/ssr"]
+67 -193
View File
@@ -2,209 +2,83 @@
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
use convert_case::{Case, Converter};
use proc_macro::TokenStream;
use proc_macro2::Literal;
use quote::{ToTokens, __private::TokenStream as TokenStream2};
use server_fn_macro::*;
use syn::{
parse::{Parse, ParseStream},
Ident, ItemFn, Token,
};
//! This crate contains the dioxus implementation of the #[macro@crate::server] macro without additional context from the server.
//! See the [server_fn_macro] crate for more information.
/// Declares that a function is a [server function](https://dioxuslabs.com/learn/0.4/reference/fullstack/server_functions). This means that
/// its body will only run on the server, i.e., when the `ssr` feature is enabled.
use proc_macro::TokenStream;
use server_fn_macro::server_macro_impl;
use syn::__private::ToTokens;
/// Declares that a function is a [server function](https://docs.rs/server_fn/).
/// This means that its body will only run on the server, i.e., when the `ssr`
/// feature is enabled on this crate.
///
/// If you call a server function from the client (i.e., when the `csr` or `hydrate` features
/// are enabled), it will instead make a network request to the server.
///
/// You can specify one, two, or three arguments to the server function:
/// 1. *Optional*: A type name that will be used to identify and register the server function
/// (e.g., `MyServerFn`).
/// 2. *Optional*: A URL prefix at which the function will be mounted when it’s registered
/// (e.g., `"/api"`). Defaults to `"/"`.
/// 3. *Optional*: either `"Cbor"` (specifying that it should use the binary `cbor` format for
/// serialization), `"Url"` (specifying that it should be use a URL-encoded form-data string).
/// Defaults to `"Url"`. If you want to use this server function
/// using Get instead of Post methods, the encoding must be `"GetCbor"` or `"GetJson"`.
///
/// The server function itself can take any number of arguments, each of which should be serializable
/// and deserializable with `serde`. Optionally, its first argument can be a [DioxusServerContext](https::/docs.rs/dioxus-fullstack/latest/dixous_server/prelude/struct.DioxusServerContext.html),
/// which will be injected *on the server side.* This can be used to inject the raw HTTP request or other
/// server-side context into the server function.
///
/// ```ignore
/// # use dioxus_fullstack::prelude::*; use serde::{Serialize, Deserialize};
/// # #[derive(Serialize, Deserialize)]
/// # pub struct Post { }
/// #[server(ReadPosts, "/api")]
/// pub async fn read_posts(how_many: u8, query: String) -> Result<Vec<Post>, ServerFnError> {
/// // do some work on the server to access the database
/// # unimplemented!()
/// ## Usage
/// ```rust,ignore
/// #[server]
/// pub async fn blog_posts(
/// category: String,
/// ) -> Result<Vec<BlogPost>, ServerFnError> {
/// let posts = load_posts(&category).await?;
/// // maybe do some other work
/// Ok(posts)
/// }
/// ```
///
/// Note the following:
/// - **Server functions must be `async`.** Even if the work being done inside the function body
/// can run synchronously on the server, from the client’s perspective it involves an asynchronous
/// function call.
/// - **Server functions must return `Result<T, ServerFnError>`.** Even if the work being done
/// inside the function body can’t fail, the processes of serialization/deserialization and the
/// network call are fallible.
/// - **Return types must implement [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html).**
/// This should be fairly obvious: we have to serialize arguments to send them to the server, and we
/// need to deserialize the result to return it to the client.
/// - **Arguments must be implement [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html)
/// and [`DeserializeOwned`](https://docs.rs/serde/latest/serde/de/trait.DeserializeOwned.html).**
/// They are serialized as an `application/x-www-form-urlencoded`
/// form data using [`serde_urlencoded`](https://docs.rs/serde_urlencoded/latest/serde_urlencoded/) or as `application/cbor`
/// using [`cbor`](https://docs.rs/cbor/latest/cbor/).
/// - **The [DioxusServerContext](https::/docs.rs/dioxus-fullstack/latest/dixous_server/prelude/struct.DioxusServerContext.html) comes from the server.** Optionally, the first argument of a server function
/// can be a [DioxusServerContext](https::/docs.rs/dioxus-fullstack/latest/dixous_server/prelude/struct.DioxusServerContext.html). This scope can be used to inject dependencies like the HTTP request
/// or response or other server-only dependencies, but it does *not* have access to reactive state that exists in the client.
/// ## Named Arguments
///
/// You can any combination of the following named arguments:
/// - `name`: sets the identifier for the server function’s type, which is a struct created
/// to hold the arguments (defaults to the function identifier in PascalCase)
/// - `prefix`: a prefix at which the server function handler will be mounted (defaults to `/api`)
/// - `endpoint`: specifies the exact path at which the server function handler will be mounted,
/// relative to the prefix (defaults to the function name followed by unique hash)
/// - `input`: the encoding for the arguments (defaults to `PostUrl`)
/// - `output`: the encoding for the response (defaults to `Json`)
/// - `client`: a custom `Client` implementation that will be used for this server fn
/// - `encoding`: (legacy, may be deprecated in future) specifies the encoding, which may be one
/// of the following (not case sensitive)
/// - `"Url"`: `POST` request with URL-encoded arguments and JSON response
/// - `"GetUrl"`: `GET` request with URL-encoded arguments and JSON response
/// - `"Cbor"`: `POST` request with CBOR-encoded arguments and response
/// - `"GetCbor"`: `GET` request with URL-encoded arguments and CBOR response
/// - `req` and `res` specify the HTTP request and response types to be used on the server (these
/// should usually only be necessary if you are integrating with a server other than Actix/Axum)
/// ```rust,ignore
/// #[server(
/// name = SomeStructName,
/// prefix = "/my_api",
/// endpoint = "my_fn",
/// input = Cbor,
/// output = Json
/// )]
/// pub async fn my_wacky_server_fn(input: Vec<String>) -> Result<usize, ServerFnError> {
/// todo!()
/// }
///
/// // expands to
/// #[derive(Deserialize, Serialize)]
/// struct SomeStructName {
/// input: Vec<String>
/// }
///
/// impl ServerFn for SomeStructName {
/// const PATH: &'static str = "/my_api/my_fn";
///
/// // etc.
/// }
/// ```
#[proc_macro_attribute]
pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
// before we pass this off to the server function macro, we apply extractors and middleware
let mut function: syn::ItemFn = match syn::parse(s).map_err(|e| e.to_compile_error()) {
Ok(f) => f,
Err(e) => return e.into(),
};
// extract all #[middleware] attributes
let mut middlewares: Vec<Middleware> = vec![];
function.attrs.retain(|attr| {
if attr.meta.path().is_ident("middleware") {
if let Ok(middleware) = attr.parse_args() {
middlewares.push(middleware);
false
} else {
true
}
} else {
true
}
});
let ItemFn {
attrs,
vis,
sig,
block,
} = function;
let mapped_body = quote::quote! {
#(#attrs)*
#vis #sig {
#block
}
};
let server_fn_path: syn::Path = syn::parse_quote!(::dioxus::fullstack::prelude::server_fn);
let trait_obj_wrapper: syn::Type =
syn::parse_quote!(::dioxus::fullstack::prelude::ServerFnTraitObj);
let mut args: ServerFnArgs = match syn::parse(args) {
Ok(args) => args,
Err(e) => return e.to_compile_error().into(),
};
if args.struct_name.is_none() {
let upper_cammel_case_name = Converter::new()
.from_case(Case::Snake)
.to_case(Case::UpperCamel)
.convert(sig.ident.to_string());
args.struct_name = Some(Ident::new(&upper_cammel_case_name, sig.ident.span()));
}
let struct_name = args.struct_name.as_ref().unwrap();
match server_macro_impl(
quote::quote!(#args),
mapped_body,
trait_obj_wrapper,
args.into(),
s.into(),
Some(syn::parse_quote!(server_fn)),
"/api",
None,
None,
Some(server_fn_path.clone()),
) {
Err(e) => e.to_compile_error().into(),
Ok(tokens) => quote::quote! {
#tokens
#[cfg(feature = "server")]
#server_fn_path::inventory::submit! {
::dioxus::fullstack::prelude::ServerFnMiddleware {
prefix: #struct_name::PREFIX,
url: #struct_name::URL,
middleware: || vec![
#(
std::sync::Arc::new(#middlewares),
),*
]
}
}
}
.to_token_stream()
.into(),
}
}
#[derive(Debug)]
struct Middleware {
expr: syn::Expr,
}
impl ToTokens for Middleware {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let expr = &self.expr;
tokens.extend(quote::quote! {
#expr
});
}
}
impl Parse for Middleware {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let arg: syn::Expr = input.parse()?;
Ok(Middleware { expr: arg })
}
}
struct ServerFnArgs {
struct_name: Option<Ident>,
_comma: Option<Token![,]>,
prefix: Option<Literal>,
_comma2: Option<Token![,]>,
encoding: Option<Literal>,
_comma3: Option<Token![,]>,
fn_path: Option<Literal>,
}
impl ToTokens for ServerFnArgs {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let struct_name = self.struct_name.as_ref().map(|s| quote::quote! { #s, });
let prefix = self.prefix.as_ref().map(|p| quote::quote! { #p, });
let encoding = self.encoding.as_ref().map(|e| quote::quote! { #e, });
let fn_path = self.fn_path.as_ref().map(|f| quote::quote! { #f, });
tokens.extend(quote::quote! {
#struct_name
#prefix
#encoding
#fn_path
})
}
}
impl Parse for ServerFnArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let struct_name = input.parse()?;
let _comma = input.parse()?;
let prefix = input.parse()?;
let _comma2 = input.parse()?;
let encoding = input.parse()?;
let _comma3 = input.parse()?;
let fn_path = input.parse()?;
Ok(Self {
struct_name,
_comma,
prefix,
_comma2,
encoding,
_comma3,
fn_path,
})
Ok(s) => s.to_token_stream().into(),
}
}
+6 -5
View File
@@ -15,11 +15,11 @@ generational-box = { workspace = true }
askama_escape = "0.10.3"
thiserror = "1.0.23"
rustc-hash = "1.1.0"
lru = "0.10.0"
lru = { workspace = true }
tracing = { workspace = true }
http = "0.2.9"
async-trait = "0.1.58"
serde_json = { version = "1.0" }
http = { workspace = true }
async-trait = { workspace = true }
serde_json = { workspace = true }
chrono = { verison = "0.4.34", optional = true }
[target.'cfg(target_arch = "wasm32")'.dependencies]
@@ -30,6 +30,8 @@ tokio = { version = "1.28", features = ["fs", "io-util"], optional = true }
[dev-dependencies]
dioxus = { workspace = true }
dioxus-signals = { workspace = true }
tracing = { workspace = true }
fern = { version = "0.6.0", features = ["colored"] }
anyhow = "1.0"
@@ -37,7 +39,6 @@ argh = "0.1.4"
serde = "1.0.120"
serde_json = "1.0.61"
fs_extra = "1.2.0"
dioxus-signals = { workspace = true }
[features]
default = []
+1
View File
@@ -103,6 +103,7 @@ impl ValidCachedPath {
})
}
#[cfg(not(target_arch = "wasm32"))]
pub fn freshness(&self, max_age: Option<std::time::Duration>) -> Option<RenderFreshness> {
let age = self.timestamp.elapsed().ok()?.as_secs();
let max_age = max_age.map(|max_age| max_age.as_secs());
+6 -3
View File
@@ -10,13 +10,12 @@ use rustc_hash::FxHasher;
use std::{
future::Future,
hash::BuildHasherDefault,
io::Write,
ops::{Deref, DerefMut},
path::PathBuf,
pin::Pin,
time::{Duration, SystemTime},
};
use tokio::io::{AsyncWrite, AsyncWriteExt, BufReader};
use tokio::io::{AsyncWrite, AsyncWriteExt};
pub use crate::fs_cache::*;
pub use crate::incremental_cfg::*;
@@ -67,6 +66,7 @@ impl IncrementalRenderer {
let _ = std::fs::remove_dir_all(&self.static_dir);
}
#[cfg(not(target_arch = "wasm32"))]
fn track_timestamps(&self) -> bool {
self.invalidate_after.is_some()
}
@@ -102,6 +102,7 @@ impl IncrementalRenderer {
) -> Result<RenderFreshness, IncrementalRendererError> {
#[cfg(not(target_arch = "wasm32"))]
{
use std::io::Write;
let file_path = self.route_as_path(&route);
if let Some(parent) = file_path.parent() {
if !parent.exists() {
@@ -122,6 +123,7 @@ impl IncrementalRenderer {
}
}
#[cfg(not(target_arch = "wasm32"))]
fn promote_memory_cache<K: AsRef<str>>(&mut self, route: K) {
if let Some(cache) = self.memory_cache.as_mut() {
cache.promote(route.as_ref())
@@ -160,7 +162,7 @@ impl IncrementalRenderer {
if let Some(file_path) = self.find_file(&route) {
if let Some(freshness) = file_path.freshness(self.invalidate_after) {
if let Ok(file) = tokio::fs::File::open(file_path.full_path).await {
let mut file = BufReader::new(file);
let mut file = tokio::io::BufReader::new(file);
tokio::io::copy_buf(&mut file, output).await?;
tracing::trace!("file cache hit {:?}", route);
self.promote_memory_cache(&route);
@@ -229,6 +231,7 @@ impl IncrementalRenderer {
}
}
#[cfg(not(target_arch = "wasm32"))]
fn route_as_path(&self, route: &str) -> PathBuf {
let mut file_path = (self.map_path)(route);
if self.track_timestamps() {