Merge branch 'master' into feature/use-shared-state-better-diagnostics
This commit is contained in:
324 files changed
+13509
-6646
No files matched your search
@@ -3,35 +3,40 @@ name = "dioxus-fullstack"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Fullstack Dioxus Utilities"
|
||||
license = "MIT/Apache-2.0"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||
homepage = "https://dioxuslabs.com"
|
||||
keywords = ["dom", "ui", "gui", "react", "ssr", "fullstack"]
|
||||
|
||||
[dependencies]
|
||||
# server functions
|
||||
server_fn = { git = "https://github.com/leptos-rs/leptos", rev = "15a4e54435eb5a539afb75891292bcccd2cc8e85", default-features = false, features = ["stable"] }
|
||||
server_fn = { git = "https://github.com/leptos-rs/leptos", rev = "6b90e1babd425c9a324181c86e3fd1b942c9b10f", default-features = false }
|
||||
dioxus_server_macro = { path = "server-macro" }
|
||||
|
||||
# warp
|
||||
warp = { version = "0.3.3", optional = true }
|
||||
warp = { version = "0.3.5", features = ["compression-gzip"], optional = true }
|
||||
http-body = { version = "0.4.5", optional = true }
|
||||
http-body-util = "0.1.0-rc.2"
|
||||
|
||||
# axum
|
||||
axum = { version = "0.6.1", features = ["ws"], optional = true }
|
||||
tower-http = { version = "0.4.0", optional = true, features = ["fs"] }
|
||||
axum = { version = "0.6.1", features = ["ws", "macros"], optional = true }
|
||||
tower-http = { version = "0.4.0", optional = true, features = ["fs", "compression-gzip"] }
|
||||
tower = { version = "0.4.13", features = ["util"], optional = true }
|
||||
axum-macros = "0.3.7"
|
||||
|
||||
# salvo
|
||||
salvo = { version = "0.37.7", optional = true, features = ["serve-static", "ws"] }
|
||||
salvo = { version = "0.46.0", optional = true, features = ["serve-static", "websocket", "compression"] }
|
||||
serde = "1.0.159"
|
||||
|
||||
# Dioxus + SSR
|
||||
dioxus-core = { workspace = true }
|
||||
dioxus = { workspace = true }
|
||||
dioxus-ssr = { workspace = true, optional = true }
|
||||
hyper = { version = "0.14.25", optional = true }
|
||||
http = { version = "0.2.9", optional = true }
|
||||
|
||||
# Router Intigration
|
||||
dioxus-router = { workspace = true, optional = true }
|
||||
|
||||
log = { workspace = true }
|
||||
once_cell = "1.17.1"
|
||||
thiserror = { workspace = true }
|
||||
@@ -43,9 +48,13 @@ serde_json = { version = "1.0.95", optional = true }
|
||||
tokio-stream = { version = "0.1.12", features = ["sync"], optional = true }
|
||||
futures-util = { workspace = true, optional = true }
|
||||
postcard = { version = "1.0.4", features = ["use-std"] }
|
||||
yazi = "0.1.5"
|
||||
base64 = "0.21.0"
|
||||
|
||||
pin-project = "1.1.2"
|
||||
async-trait = "0.1.71"
|
||||
bytes = "1.4.0"
|
||||
tower-layer = "0.3.2"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
dioxus-hot-reload = { workspace = true }
|
||||
|
||||
@@ -54,10 +63,14 @@ web-sys = { version = "0.3.61", features = ["Window", "Document", "Element", "Ht
|
||||
|
||||
[features]
|
||||
default = ["hot-reload", "default-tls"]
|
||||
hot-reload = ["serde_json", "tokio-stream", "futures-util"]
|
||||
warp = ["dep:warp", "http-body", "ssr"]
|
||||
router = ["dioxus-router"]
|
||||
hot-reload = ["serde_json", "futures-util"]
|
||||
warp = ["dep:warp", "ssr"]
|
||||
axum = ["dep:axum", "tower-http", "ssr"]
|
||||
salvo = ["dep:salvo", "ssr"]
|
||||
ssr = ["server_fn/ssr", "tokio", "dioxus-ssr", "hyper", "http"]
|
||||
ssr = ["server_fn/ssr", "tokio", "dioxus-ssr", "tower", "hyper", "http", "http-body", "dioxus-router/ssr", "tokio-stream"]
|
||||
default-tls = ["server_fn/default-tls"]
|
||||
rustls = ["server_fn/rustls"]
|
||||
|
||||
[dev-dependencies]
|
||||
dioxus-fullstack = { path = ".", features = ["router"] }
|
||||
@@ -0,0 +1,3 @@
|
||||
dist
|
||||
target
|
||||
static
|
||||
@@ -0,0 +1,53 @@
|
||||
[package]
|
||||
name = "axum-auth"
|
||||
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 }
|
||||
axum = { version = "0.6.12", 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 }
|
||||
simple_logger = { version = "4.2.0", optional = true }
|
||||
async-trait = { version = "0.1.71", optional = true }
|
||||
sqlx = { version = "0.7.0", features = [
|
||||
"macros",
|
||||
"migrate",
|
||||
"postgres",
|
||||
"sqlite",
|
||||
"_unstable-all-types",
|
||||
"tls-rustls",
|
||||
"runtime-tokio",
|
||||
], optional = true }
|
||||
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"
|
||||
features = ["sqlite-rustls"]
|
||||
optional = true
|
||||
|
||||
[dependencies.axum_session_auth]
|
||||
version = "0.3.0"
|
||||
features = ["sqlite-rustls"]
|
||||
optional = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
ssr = ["axum", "tokio", "dioxus-fullstack/axum", "tower-http", "simple_logger", "async-trait", "sqlx", "axum_session", "axum_session_auth", "http", "tower"]
|
||||
web = ["dioxus-web"]
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
panic = "abort"
|
||||
opt-level = 'z'
|
||||
strip = true
|
||||
codegen-units = 1
|
||||
@@ -0,0 +1,262 @@
|
||||
use async_trait::async_trait;
|
||||
use axum::{
|
||||
http::Method,
|
||||
response::{IntoResponse, Response},
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use axum_session::{SessionConfig, SessionLayer, SessionSqlitePool, SessionStore};
|
||||
use axum_session_auth::*;
|
||||
use core::pin::Pin;
|
||||
use dioxus_fullstack::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions};
|
||||
use std::error::Error;
|
||||
use std::future::Future;
|
||||
use std::{collections::HashSet, net::SocketAddr, str::FromStr};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct User {
|
||||
pub id: i32,
|
||||
pub anonymous: bool,
|
||||
pub username: String,
|
||||
pub permissions: HashSet<String>,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Clone)]
|
||||
pub struct SqlPermissionTokens {
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
impl Default for User {
|
||||
fn default() -> Self {
|
||||
let mut permissions = HashSet::new();
|
||||
|
||||
permissions.insert("Category::View".to_owned());
|
||||
|
||||
Self {
|
||||
id: 1,
|
||||
anonymous: true,
|
||||
username: "Guest".into(),
|
||||
permissions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Authentication<User, i64, SqlitePool> for User {
|
||||
async fn load_user(userid: i64, pool: Option<&SqlitePool>) -> Result<User, anyhow::Error> {
|
||||
let pool = pool.unwrap();
|
||||
|
||||
User::get_user(userid, pool)
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("Could not load user"))
|
||||
}
|
||||
|
||||
fn is_authenticated(&self) -> bool {
|
||||
!self.anonymous
|
||||
}
|
||||
|
||||
fn is_active(&self) -> bool {
|
||||
!self.anonymous
|
||||
}
|
||||
|
||||
fn is_anonymous(&self) -> bool {
|
||||
self.anonymous
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl HasPermission<SqlitePool> for User {
|
||||
async fn has(&self, perm: &str, _pool: &Option<&SqlitePool>) -> bool {
|
||||
self.permissions.contains(perm)
|
||||
}
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub async fn get_user(id: i64, pool: &SqlitePool) -> Option<Self> {
|
||||
let sqluser = sqlx::query_as::<_, SqlUser>("SELECT * FROM users WHERE id = $1")
|
||||
.bind(id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
//lets just get all the tokens the user can use, we will only use the full permissions if modifing them.
|
||||
let sql_user_perms = sqlx::query_as::<_, SqlPermissionTokens>(
|
||||
"SELECT token FROM user_permissions WHERE user_id = $1;",
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
Some(sqluser.into_user(Some(sql_user_perms)))
|
||||
}
|
||||
|
||||
pub async fn create_user_tables(pool: &SqlitePool) {
|
||||
sqlx::query(
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"anonymous" BOOLEAN NOT NULL,
|
||||
"username" VARCHAR(256) NOT NULL
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS user_permissions (
|
||||
"user_id" INTEGER NOT NULL,
|
||||
"token" VARCHAR(256) NOT NULL
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO users
|
||||
(id, anonymous, username) SELECT 1, true, 'Guest'
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
anonymous = EXCLUDED.anonymous,
|
||||
username = EXCLUDED.username
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO users
|
||||
(id, anonymous, username) SELECT 2, false, 'Test'
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
anonymous = EXCLUDED.anonymous,
|
||||
username = EXCLUDED.username
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO user_permissions
|
||||
(user_id, token) SELECT 2, 'Category::View'
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Clone)]
|
||||
pub struct SqlUser {
|
||||
pub id: i32,
|
||||
pub anonymous: bool,
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
impl SqlUser {
|
||||
pub fn into_user(self, sql_user_perms: Option<Vec<SqlPermissionTokens>>) -> User {
|
||||
User {
|
||||
id: self.id,
|
||||
anonymous: self.anonymous,
|
||||
username: self.username,
|
||||
permissions: if let Some(user_perms) = sql_user_perms {
|
||||
user_perms
|
||||
.into_iter()
|
||||
.map(|x| x.token)
|
||||
.collect::<HashSet<String>>()
|
||||
} else {
|
||||
HashSet::<String>::new()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn connect_to_database() -> SqlitePool {
|
||||
let connect_opts = SqliteConnectOptions::from_str("sqlite::memory:").unwrap();
|
||||
|
||||
SqlitePoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect_with(connect_opts)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub struct Session(
|
||||
pub axum_session_auth::AuthSession<
|
||||
crate::auth::User,
|
||||
i64,
|
||||
axum_session_auth::SessionSqlitePool,
|
||||
sqlx::SqlitePool,
|
||||
>,
|
||||
);
|
||||
|
||||
impl std::ops::Deref for Session {
|
||||
type Target = axum_session_auth::AuthSession<
|
||||
crate::auth::User,
|
||||
i64,
|
||||
axum_session_auth::SessionSqlitePool,
|
||||
sqlx::SqlitePool,
|
||||
>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for Session {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AuthSessionLayerNotFound;
|
||||
|
||||
impl std::fmt::Display for AuthSessionLayerNotFound {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "AuthSessionLayer was not found")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for AuthSessionLayerNotFound {}
|
||||
|
||||
impl IntoResponse for AuthSessionLayerNotFound {
|
||||
fn into_response(self) -> Response {
|
||||
(
|
||||
http::status::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"AuthSessionLayer was not found",
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<S: std::marker::Sync + std::marker::Send> axum::extract::FromRequestParts<S> for Session {
|
||||
type Rejection = AuthSessionLayerNotFound;
|
||||
|
||||
async fn from_request_parts(
|
||||
parts: &mut http::request::Parts,
|
||||
state: &S,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
axum_session_auth::AuthSession::<
|
||||
crate::auth::User,
|
||||
i64,
|
||||
axum_session_auth::SessionSqlitePool,
|
||||
sqlx::SqlitePool,
|
||||
>::from_request_parts(parts, state)
|
||||
.await
|
||||
.map(Session)
|
||||
.map_err(|_| AuthSessionLayerNotFound)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
//! Run with:
|
||||
//!
|
||||
//! ```sh
|
||||
//! dx build --features web
|
||||
//! cargo run --features ssr
|
||||
//! ```
|
||||
|
||||
#![allow(non_snake_case, unused)]
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
mod auth;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_fullstack::prelude::*;
|
||||
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));
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
use crate::auth::*;
|
||||
use axum::routing::*;
|
||||
use axum_session::SessionConfig;
|
||||
use axum_session::SessionStore;
|
||||
use axum_session_auth::AuthConfig;
|
||||
use axum_session_auth::SessionSqlitePool;
|
||||
simple_logger::SimpleLogger::new().init().unwrap();
|
||||
tokio::runtime::Runtime::new()
|
||||
.unwrap()
|
||||
.block_on(async move {
|
||||
let pool = connect_to_database().await;
|
||||
|
||||
//This Defaults as normal Cookies.
|
||||
//To enable Private cookies for integrity, and authenticity please check the next Example.
|
||||
let session_config = SessionConfig::default().with_table_name("test_table");
|
||||
let auth_config = AuthConfig::<i64>::default().with_anonymous_user_id(Some(1));
|
||||
let session_store = SessionStore::<SessionSqlitePool>::new(
|
||||
Some(pool.clone().into()),
|
||||
session_config,
|
||||
)
|
||||
.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("", ServeConfigBuilder::new(app, ()))
|
||||
.layer(
|
||||
axum_session_auth::AuthSessionLayer::<
|
||||
crate::auth::User,
|
||||
i64,
|
||||
axum_session_auth::SessionSqlitePool,
|
||||
sqlx::SqlitePool,
|
||||
>::new(Some(pool))
|
||||
.with_config(auth_config),
|
||||
)
|
||||
.layer(axum_session::SessionLayer::new(session_store));
|
||||
|
||||
// run it
|
||||
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 3000));
|
||||
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
let user_name = use_state(cx, || "?".to_string());
|
||||
let permissions = use_state(cx, || "?".to_string());
|
||||
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
button {
|
||||
onclick: move |_| {
|
||||
async move {
|
||||
login().await.unwrap();
|
||||
}
|
||||
},
|
||||
"Login Test User"
|
||||
}
|
||||
}
|
||||
div {
|
||||
button {
|
||||
onclick: move |_| {
|
||||
to_owned![user_name];
|
||||
async move {
|
||||
if let Ok(data) = get_user_name().await {
|
||||
user_name.set(data);
|
||||
}
|
||||
}
|
||||
},
|
||||
"Get User Name"
|
||||
}
|
||||
"User name: {user_name}"
|
||||
}
|
||||
div {
|
||||
button {
|
||||
onclick: move |_| {
|
||||
to_owned![permissions];
|
||||
async move {
|
||||
if let Ok(data) = get_permissions().await {
|
||||
permissions.set(data);
|
||||
}
|
||||
}
|
||||
},
|
||||
"Get Permissions"
|
||||
}
|
||||
"Permissions: {permissions}"
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[server(GetUserName)]
|
||||
pub async fn get_user_name() -> Result<String, ServerFnError> {
|
||||
let session: crate::auth::Session = extract().await?;
|
||||
Ok(session.0.current_user.unwrap().username.to_string())
|
||||
}
|
||||
|
||||
#[server(Login)]
|
||||
pub async fn login() -> Result<(), ServerFnError> {
|
||||
let auth: crate::auth::Session = extract().await?;
|
||||
auth.login_user(2);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[server(Permissions)]
|
||||
pub async fn get_permissions() -> Result<String, ServerFnError> {
|
||||
let method: axum::http::Method = extract().await?;
|
||||
let auth: crate::auth::Session = extract().await?;
|
||||
let current_user = auth.current_user.clone().unwrap_or_default();
|
||||
|
||||
// lets check permissions only and not worry about if they are anon or not
|
||||
if !axum_session_auth::Auth::<crate::auth::User, i64, sqlx::SqlitePool>::build(
|
||||
[axum::http::Method::POST],
|
||||
false,
|
||||
)
|
||||
.requires(axum_session_auth::Rights::any([
|
||||
axum_session_auth::Rights::permission("Category::View"),
|
||||
axum_session_auth::Rights::permission("Admin::View"),
|
||||
]))
|
||||
.validate(¤t_user, &method, None)
|
||||
.await
|
||||
{
|
||||
return Ok(format!(
|
||||
"User {}, Does not have permissions needed to view this page please login",
|
||||
current_user.username
|
||||
));
|
||||
}
|
||||
|
||||
Ok(format!(
|
||||
"User has Permissions needed. Here are the Users permissions: {:?}",
|
||||
current_user.permissions
|
||||
))
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// Run with:
|
||||
// ```bash
|
||||
// cargo run --bin client --features="desktop"
|
||||
// cargo run --bin client --features desktop
|
||||
// ```
|
||||
|
||||
use axum_desktop::*;
|
||||
@@ -8,6 +8,6 @@ use dioxus_fullstack::prelude::server_fn::set_server_url;
|
||||
|
||||
fn main() {
|
||||
// Set the url of the server where server functions are hosted.
|
||||
set_server_url("http://localhost:8080");
|
||||
set_server_url("http://127.0.0.0:8080");
|
||||
dioxus_desktop::launch(app)
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
// Run with:
|
||||
// ```bash
|
||||
// cargo run --bin server --features="ssr"
|
||||
// cargo run --bin server --features ssr
|
||||
// ```
|
||||
|
||||
use axum_desktop::*;
|
||||
use dioxus_fullstack::prelude::*;
|
||||
|
||||
#[tokio::main]
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
dist
|
||||
target
|
||||
target
|
||||
static
|
||||
@@ -7,15 +7,24 @@ 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-web = { workspace = true, features = ["hydrate"], optional = true }
|
||||
dioxus = { workspace = true }
|
||||
dioxus-fullstack = { workspace = true }
|
||||
axum = { version = "0.6.12", 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"] }
|
||||
simple_logger = "4.2.0"
|
||||
|
||||
[features]
|
||||
default = ["web"]
|
||||
default = []
|
||||
ssr = ["axum", "tokio", "dioxus-fullstack/axum"]
|
||||
web = ["dioxus-web"]
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
panic = "abort"
|
||||
opt-level = 'z'
|
||||
strip = true
|
||||
codegen-units = 1
|
||||
@@ -1,56 +1,15 @@
|
||||
//! Run with:
|
||||
//!
|
||||
//! ```sh
|
||||
//! dioxus build --features web
|
||||
//! cargo run --features ssr --no-default-features
|
||||
//! dx build --features web
|
||||
//! cargo run --features ssr
|
||||
//! ```
|
||||
|
||||
#![allow(non_snake_case)]
|
||||
#![allow(non_snake_case, unused)]
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_fullstack::prelude::*;
|
||||
use dioxus_fullstack::{launch, prelude::*};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
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 = "ssr")]
|
||||
{
|
||||
// Start hot reloading
|
||||
hot_reload_init!(dioxus_hot_reload::Config::new().with_rebuild_callback(|| {
|
||||
execute::shell("dioxus build --features web")
|
||||
.spawn()
|
||||
.unwrap()
|
||||
.wait()
|
||||
.unwrap();
|
||||
execute::shell("cargo run --features ssr --no-default-features")
|
||||
.spawn()
|
||||
.unwrap();
|
||||
true
|
||||
}));
|
||||
|
||||
tokio::runtime::Runtime::new()
|
||||
.unwrap()
|
||||
.block_on(async move {
|
||||
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
|
||||
axum::Server::bind(&addr)
|
||||
.serve(
|
||||
axum::Router::new()
|
||||
.serve_dioxus_application(
|
||||
"",
|
||||
ServeConfigBuilder::new(app, AppProps { count: 12345 }).build(),
|
||||
)
|
||||
.into_make_service(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Props, PartialEq, Debug, Default, Serialize, Deserialize, Clone)]
|
||||
struct AppProps {
|
||||
count: i32,
|
||||
@@ -67,12 +26,11 @@ fn app(cx: Scope<AppProps>) -> Element {
|
||||
button {
|
||||
onclick: move |_| {
|
||||
to_owned![text];
|
||||
let sc = cx.sc();
|
||||
async move {
|
||||
if let Ok(data) = get_server_data().await {
|
||||
println!("Client received: {}", data);
|
||||
text.set(data.clone());
|
||||
post_server_data(sc, data).await.unwrap();
|
||||
post_server_data(data).await.unwrap();
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -83,12 +41,10 @@ fn app(cx: Scope<AppProps>) -> Element {
|
||||
}
|
||||
|
||||
#[server(PostServerData)]
|
||||
async fn post_server_data(cx: DioxusServerContext, data: String) -> Result<(), ServerFnError> {
|
||||
// The server context contains information about the current request and allows you to modify the response.
|
||||
cx.response_headers_mut()
|
||||
.insert("Set-Cookie", "foo=bar".parse().unwrap());
|
||||
async fn post_server_data(data: String) -> Result<(), ServerFnError> {
|
||||
let axum::extract::Host(host): axum::extract::Host = extract()?;
|
||||
println!("Server received: {}", data);
|
||||
println!("Request parts are {:?}", cx.request_parts());
|
||||
println!("{:?}", host);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -97,3 +53,9 @@ async fn post_server_data(cx: DioxusServerContext, data: String) -> Result<(), S
|
||||
async fn get_server_data() -> Result<String, ServerFnError> {
|
||||
Ok("Hello from the server!".to_string())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
launch!(@([127, 0, 0, 1], 8080), app, {
|
||||
serve_cfg: ServeConfigBuilder::new(app, AppProps { count: 0 }),
|
||||
});
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
dist
|
||||
target
|
||||
target
|
||||
static
|
||||
@@ -7,18 +7,15 @@ 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-web = { workspace = true, features = ["hydrate"], optional = true }
|
||||
dioxus = { workspace = true }
|
||||
dioxus-router = { workspace = true }
|
||||
dioxus-fullstack = { workspace = true }
|
||||
dioxus-router = { workspace = true}
|
||||
dioxus-fullstack = { workspace = true, features = ["router"] }
|
||||
axum = { version = "0.6.12", optional = true }
|
||||
tokio = { workspace = true, features = ["full"], optional = true }
|
||||
serde = "1.0.159"
|
||||
tower-http = { version = "0.4.0", features = ["fs"], optional = true }
|
||||
http = { version = "0.2.9", optional = true }
|
||||
execute = "0.2.12"
|
||||
tokio = {workspace = true, features = ["full"], optional = true }
|
||||
serde = { version = "1.0.159", features = ["derive"] }
|
||||
|
||||
[features]
|
||||
default = ["web"]
|
||||
ssr = ["axum", "tokio", "dioxus-fullstack/axum", "tower-http", "http"]
|
||||
default = []
|
||||
ssr = ["axum", "tokio", "dioxus-fullstack/axum"]
|
||||
web = ["dioxus-web", "dioxus-router/web"]
|
||||
@@ -1,122 +1,61 @@
|
||||
//! Run with:
|
||||
//!
|
||||
//! ```sh
|
||||
//! dioxus build --features web
|
||||
//! cargo run --features ssr --no-default-features
|
||||
//! dx build --features web
|
||||
//! cargo run --features ssr
|
||||
//! ```
|
||||
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_fullstack::prelude::*;
|
||||
use dioxus_router::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use dioxus_router::prelude::*;
|
||||
|
||||
fn main() {
|
||||
#[cfg(feature = "web")]
|
||||
dioxus_web::launch_with_props(
|
||||
App,
|
||||
AppProps { route: None },
|
||||
dioxus_web::Config::new().hydrate(true),
|
||||
);
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
// Start hot reloading
|
||||
hot_reload_init!(dioxus_hot_reload::Config::new().with_rebuild_callback(|| {
|
||||
execute::shell("dioxus build --features web")
|
||||
.spawn()
|
||||
.unwrap()
|
||||
.wait()
|
||||
.unwrap();
|
||||
execute::shell("cargo run --features ssr --no-default-features")
|
||||
.spawn()
|
||||
.unwrap();
|
||||
true
|
||||
}));
|
||||
|
||||
use axum::extract::State;
|
||||
tokio::runtime::Runtime::new()
|
||||
.unwrap()
|
||||
.block_on(async move {
|
||||
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
|
||||
|
||||
axum::Server::bind(&addr)
|
||||
.serve(
|
||||
axum::Router::new()
|
||||
// Serve the dist/assets folder with the javascript and WASM files created by the CLI
|
||||
.serve_static_assets("./dist")
|
||||
// Register server functions
|
||||
.register_server_fns("")
|
||||
// Connect to the hot reload server
|
||||
.connect_hot_reload()
|
||||
// If the path is unknown, render the application
|
||||
.fallback(
|
||||
move |uri: http::uri::Uri, State(ssr_state): State<SSRState>| {
|
||||
let rendered = ssr_state.render(
|
||||
&ServeConfigBuilder::new(
|
||||
App,
|
||||
AppProps {
|
||||
route: Some(format!("http://{addr}{uri}")),
|
||||
},
|
||||
)
|
||||
.build(),
|
||||
);
|
||||
async move { axum::body::Full::from(rendered) }
|
||||
},
|
||||
)
|
||||
.with_state(SSRState::default())
|
||||
.into_make_service(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
launch_router!(@([127, 0, 0, 1], 8080), Route, {
|
||||
incremental: IncrementalRendererConfig::default().invalidate_after(std::time::Duration::from_secs(120)),
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Props, PartialEq, Serialize, Deserialize)]
|
||||
struct AppProps {
|
||||
route: Option<String>,
|
||||
#[derive(Clone, Routable, Debug, PartialEq)]
|
||||
enum Route {
|
||||
#[route("/")]
|
||||
Home {},
|
||||
#[route("/blog/:id")]
|
||||
Blog { id: i32 },
|
||||
}
|
||||
|
||||
fn App(cx: Scope<AppProps>) -> Element {
|
||||
cx.render(rsx! {
|
||||
Router {
|
||||
initial_url: cx.props.route.clone(),
|
||||
|
||||
Route { to: "/blog",
|
||||
Link {
|
||||
to: "/",
|
||||
"Go to counter"
|
||||
}
|
||||
table {
|
||||
tbody {
|
||||
for _ in 0..100 {
|
||||
tr {
|
||||
for _ in 0..100 {
|
||||
td { "hello world!" }
|
||||
}
|
||||
}
|
||||
#[inline_props]
|
||||
fn Blog(cx: Scope, id: i32) -> Element {
|
||||
render! {
|
||||
Link { target: Route::Home {}, "Go to counter" }
|
||||
table {
|
||||
tbody {
|
||||
for _ in 0..*id {
|
||||
tr {
|
||||
for _ in 0..*id {
|
||||
td { "hello world!" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// Fallback
|
||||
Route { to: "",
|
||||
Counter {}
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn Counter(cx: Scope) -> Element {
|
||||
#[inline_props]
|
||||
fn Home(cx: Scope) -> Element {
|
||||
let mut count = use_state(cx, || 0);
|
||||
let text = use_state(cx, || "...".to_string());
|
||||
|
||||
cx.render(rsx! {
|
||||
Link {
|
||||
to: "/blog",
|
||||
target: Route::Blog {
|
||||
id: *count.get()
|
||||
},
|
||||
"Go to blog"
|
||||
}
|
||||
div{
|
||||
div {
|
||||
h1 { "High-Five counter: {count}" }
|
||||
button { onclick: move |_| count += 1, "Up high!" }
|
||||
button { onclick: move |_| count -= 1, "Down low!" }
|
||||
@@ -131,7 +70,7 @@ fn Counter(cx: Scope) -> Element {
|
||||
}
|
||||
}
|
||||
},
|
||||
"Run a server function"
|
||||
"Run server function!"
|
||||
}
|
||||
"Server said: {text}"
|
||||
}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
dist
|
||||
target
|
||||
target
|
||||
static
|
||||
@@ -16,6 +16,6 @@ salvo = { version = "0.37.9", optional = true }
|
||||
execute = "0.2.12"
|
||||
|
||||
[features]
|
||||
default = ["web"]
|
||||
default = []
|
||||
ssr = ["salvo", "tokio", "dioxus-fullstack/salvo"]
|
||||
web = ["dioxus-web"]
|
||||
@@ -1,50 +1,19 @@
|
||||
//! Run with:
|
||||
//!
|
||||
//! ```sh
|
||||
//! dioxus build --features web
|
||||
//! cargo run --features ssr --no-default-features
|
||||
//! dx build --features web
|
||||
//! cargo run --features ssr
|
||||
//! ```
|
||||
|
||||
#![allow(non_snake_case)]
|
||||
#![allow(non_snake_case, unused)]
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_fullstack::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
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 = "ssr")]
|
||||
{
|
||||
// Start hot reloading
|
||||
hot_reload_init!(dioxus_hot_reload::Config::new().with_rebuild_callback(|| {
|
||||
execute::shell("dioxus build --features web")
|
||||
.spawn()
|
||||
.unwrap()
|
||||
.wait()
|
||||
.unwrap();
|
||||
execute::shell("cargo run --features ssr --no-default-features")
|
||||
.spawn()
|
||||
.unwrap();
|
||||
true
|
||||
}));
|
||||
|
||||
use salvo::prelude::*;
|
||||
tokio::runtime::Runtime::new()
|
||||
.unwrap()
|
||||
.block_on(async move {
|
||||
let router = Router::new().serve_dioxus_application(
|
||||
"",
|
||||
ServeConfigBuilder::new(app, AppProps { count: 12345 }),
|
||||
);
|
||||
Server::new(TcpListener::bind("127.0.0.1:8080"))
|
||||
.serve(router)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
launch!(@([127, 0, 0, 1], 8080), app, (AppProps { count: 5 }), {
|
||||
incremental: IncrementalRendererConfig::default().invalidate_after(std::time::Duration::from_secs(120)),
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Props, PartialEq, Debug, Default, Serialize, Deserialize, Clone)]
|
||||
@@ -55,7 +24,6 @@ struct AppProps {
|
||||
fn app(cx: Scope<AppProps>) -> Element {
|
||||
let mut count = use_state(cx, || cx.props.count);
|
||||
let text = use_state(cx, || "...".to_string());
|
||||
let server_context = cx.sc();
|
||||
|
||||
cx.render(rsx! {
|
||||
h1 { "High-Five counter: {count}" }
|
||||
@@ -63,12 +31,12 @@ fn app(cx: Scope<AppProps>) -> Element {
|
||||
button { onclick: move |_| count -= 1, "Down low!" }
|
||||
button {
|
||||
onclick: move |_| {
|
||||
to_owned![text, server_context];
|
||||
to_owned![text];
|
||||
async move {
|
||||
if let Ok(data) = get_server_data().await {
|
||||
println!("Client received: {}", data);
|
||||
text.set(data.clone());
|
||||
post_server_data(server_context, data).await.unwrap();
|
||||
post_server_data(data).await.unwrap();
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -79,10 +47,9 @@ fn app(cx: Scope<AppProps>) -> Element {
|
||||
}
|
||||
|
||||
#[server(PostServerData)]
|
||||
async fn post_server_data(cx: DioxusServerContext, data: String) -> Result<(), ServerFnError> {
|
||||
async fn post_server_data(data: String) -> Result<(), ServerFnError> {
|
||||
// The server context contains information about the current request and allows you to modify the response.
|
||||
cx.response_headers_mut()
|
||||
.insert("Set-Cookie", "foo=bar".parse().unwrap());
|
||||
let cx = server_context();
|
||||
println!("Server received: {}", data);
|
||||
println!("Request parts are {:?}", cx.request_parts());
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
docs
|
||||
target
|
||||
static
|
||||
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "static-hydrated"
|
||||
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, features = ["router"] }
|
||||
dioxus-router = { workspace = true}
|
||||
tokio = { workspace = true, features = ["full"], optional = true }
|
||||
serde = "1.0.159"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
ssr = ["tokio", "dioxus-fullstack/ssr"]
|
||||
web = ["dioxus-web"]
|
||||
@@ -0,0 +1,46 @@
|
||||
[application]
|
||||
|
||||
# App (Project) Name
|
||||
name = "Dioxus"
|
||||
|
||||
# Dioxus App Default Platform
|
||||
# desktop, web, mobile, ssr
|
||||
default_platform = "web"
|
||||
|
||||
# `build` & `serve` dist path
|
||||
out_dir = "docs"
|
||||
|
||||
# resource (public) file folder
|
||||
asset_dir = "public"
|
||||
|
||||
[web.app]
|
||||
|
||||
# HTML title tag content
|
||||
title = "dioxus | ⛺"
|
||||
|
||||
[web.watcher]
|
||||
|
||||
# when watcher trigger, regenerate the `index.html`
|
||||
reload_html = true
|
||||
|
||||
# which files or dirs will be watcher monitoring
|
||||
watch_path = ["src", "public"]
|
||||
|
||||
# include `assets` in web platform
|
||||
[web.resource]
|
||||
|
||||
# CSS style file
|
||||
style = ["tailwind.css"]
|
||||
|
||||
# Javascript code file
|
||||
script = []
|
||||
|
||||
[web.resource.dev]
|
||||
|
||||
# serve: [dev-server] only
|
||||
|
||||
# CSS style file
|
||||
style = []
|
||||
|
||||
# Javascript code file
|
||||
script = []
|
||||
@@ -0,0 +1,86 @@
|
||||
//! Run with:
|
||||
//!
|
||||
//! ```sh
|
||||
//! dx build --features web
|
||||
//! cargo run --features ssr
|
||||
//! ```
|
||||
|
||||
#![allow(non_snake_case, unused)]
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_fullstack::{launch, prelude::*};
|
||||
use dioxus_router::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// Generate all routes and output them to the docs path
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
pre_cache_static_routes_with_props(
|
||||
&ServeConfigBuilder::new_with_router(dioxus_fullstack::router::FullstackRouterConfig::<
|
||||
Route,
|
||||
>::default())
|
||||
.assets_path("docs")
|
||||
.incremental(IncrementalRendererConfig::default().static_dir("docs"))
|
||||
.build(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Hydrate the page
|
||||
#[cfg(feature = "web")]
|
||||
fn main() {
|
||||
dioxus_web::launch_with_props(
|
||||
dioxus_fullstack::router::RouteWithCfg::<Route>,
|
||||
dioxus_fullstack::prelude::get_root_props_from_document()
|
||||
.expect("Failed to get root props from document"),
|
||||
dioxus_web::Config::default().hydrate(true),
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "web", feature = "ssr")))]
|
||||
fn main() {}
|
||||
|
||||
#[derive(Clone, Routable, Debug, PartialEq, Serialize, Deserialize)]
|
||||
enum Route {
|
||||
#[route("/")]
|
||||
Home {},
|
||||
#[route("/blog")]
|
||||
Blog,
|
||||
}
|
||||
|
||||
#[inline_props]
|
||||
fn Blog(cx: Scope) -> Element {
|
||||
render! {
|
||||
Link { target: Route::Home {}, "Go to counter" }
|
||||
table {
|
||||
tbody {
|
||||
for _ in 0..100 {
|
||||
tr {
|
||||
for _ in 0..100 {
|
||||
td { "hello world!" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline_props]
|
||||
fn Home(cx: Scope) -> Element {
|
||||
let mut count = use_state(cx, || 0);
|
||||
let text = use_state(cx, || "...".to_string());
|
||||
|
||||
cx.render(rsx! {
|
||||
Link {
|
||||
target: Route::Blog {},
|
||||
"Go to blog"
|
||||
}
|
||||
div {
|
||||
h1 { "High-Five counter: {count}" }
|
||||
button { onclick: move |_| count += 1, "Up high!" }
|
||||
button { onclick: move |_| count -= 1, "Down low!" }
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
dist
|
||||
target
|
||||
target
|
||||
static
|
||||
@@ -16,6 +16,6 @@ warp = { version = "0.3.3", optional = true }
|
||||
execute = "0.2.12"
|
||||
|
||||
[features]
|
||||
default = ["web"]
|
||||
default = []
|
||||
ssr = ["warp", "tokio", "dioxus-fullstack/warp"]
|
||||
web = ["dioxus-web"]
|
||||
@@ -1,47 +1,19 @@
|
||||
//! Run with:
|
||||
//!
|
||||
//! ```sh
|
||||
//! dioxus build --features web
|
||||
//! cargo run --features ssr --no-default-features
|
||||
//! dx build --features web
|
||||
//! cargo run --features ssr
|
||||
//! ```
|
||||
|
||||
#![allow(non_snake_case)]
|
||||
#![allow(non_snake_case, unused)]
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_fullstack::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
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 = "ssr")]
|
||||
{
|
||||
// Start hot reloading
|
||||
hot_reload_init!(dioxus_hot_reload::Config::new().with_rebuild_callback(|| {
|
||||
execute::shell("dioxus build --features web")
|
||||
.spawn()
|
||||
.unwrap()
|
||||
.wait()
|
||||
.unwrap();
|
||||
execute::shell("cargo run --features ssr --no-default-features")
|
||||
.spawn()
|
||||
.unwrap();
|
||||
true
|
||||
}));
|
||||
|
||||
tokio::runtime::Runtime::new()
|
||||
.unwrap()
|
||||
.block_on(async move {
|
||||
let routes = serve_dioxus_application(
|
||||
"",
|
||||
ServeConfigBuilder::new(app, AppProps { count: 12345 }),
|
||||
);
|
||||
warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
|
||||
});
|
||||
}
|
||||
launch!(@([127, 0, 0, 1], 8080), app, (AppProps { count: 5 }), {
|
||||
incremental: IncrementalRendererConfig::default().invalidate_after(std::time::Duration::from_secs(120)),
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Props, PartialEq, Debug, Default, Serialize, Deserialize, Clone)]
|
||||
@@ -52,20 +24,19 @@ struct AppProps {
|
||||
fn app(cx: Scope<AppProps>) -> Element {
|
||||
let mut count = use_state(cx, || cx.props.count);
|
||||
let text = use_state(cx, || "...".to_string());
|
||||
let server_context = cx.sc();
|
||||
|
||||
cx.render(rsx! {
|
||||
h1 { "High-Five counter: {count}" }
|
||||
button { onclick: move |_| count += 10, "Up high!" }
|
||||
button { onclick: move |_| count += 1, "Up high!" }
|
||||
button { onclick: move |_| count -= 1, "Down low!" }
|
||||
button {
|
||||
onclick: move |_| {
|
||||
to_owned![text, server_context];
|
||||
to_owned![text];
|
||||
async move {
|
||||
if let Ok(data) = get_server_data().await {
|
||||
println!("Client received: {}", data);
|
||||
text.set(data.clone());
|
||||
post_server_data(server_context, data).await.unwrap();
|
||||
post_server_data(data).await.unwrap();
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -76,10 +47,9 @@ fn app(cx: Scope<AppProps>) -> Element {
|
||||
}
|
||||
|
||||
#[server(PostServerData)]
|
||||
async fn post_server_data(cx: DioxusServerContext, data: String) -> Result<(), ServerFnError> {
|
||||
async fn post_server_data(data: String) -> Result<(), ServerFnError> {
|
||||
// The server context contains information about the current request and allows you to modify the response.
|
||||
cx.response_headers_mut()
|
||||
.insert("Set-Cookie", "foo=bar".parse().unwrap());
|
||||
let cx = server_context();
|
||||
println!("Server received: {}", data);
|
||||
println!("Request parts are {:?}", cx.request_parts());
|
||||
|
||||
|
||||
@@ -6,9 +6,11 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1.0.63"
|
||||
quote = "1.0.26"
|
||||
server_fn_macro = { git = "https://github.com/leptos-rs/leptos", rev = "15a4e54435eb5a539afb75891292bcccd2cc8e85", features = ["stable"] }
|
||||
server_fn_macro = { git = "https://github.com/leptos-rs/leptos", rev = "6b90e1babd425c9a324181c86e3fd1b942c9b10f" }
|
||||
syn = { version = "2", features = ["full"] }
|
||||
convert_case = "0.6.0"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
@@ -1,6 +1,12 @@
|
||||
use convert_case::{Case, Converter};
|
||||
use proc_macro::TokenStream;
|
||||
use quote::ToTokens;
|
||||
use proc_macro2::Literal;
|
||||
use quote::{ToTokens, __private::TokenStream as TokenStream2};
|
||||
use server_fn_macro::*;
|
||||
use syn::{
|
||||
parse::{Parse, ParseStream},
|
||||
Ident, ItemFn, Token,
|
||||
};
|
||||
|
||||
/// Declares that a function is a [server function](dioxus_fullstack). This means that
|
||||
/// its body will only run on the server, i.e., when the `ssr` feature is enabled.
|
||||
@@ -54,18 +60,147 @@ use server_fn_macro::*;
|
||||
/// or response or other server-only dependencies, but it does *not* have access to reactive state that exists in the client.
|
||||
#[proc_macro_attribute]
|
||||
pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
let context = ServerContext {
|
||||
ty: syn::parse_quote!(DioxusServerContext),
|
||||
path: syn::parse_quote!(::dioxus_fullstack::prelude::DioxusServerContext),
|
||||
// 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(
|
||||
args.into(),
|
||||
s.into(),
|
||||
syn::parse_quote!(::dioxus_fullstack::prelude::ServerFnTraitObj),
|
||||
Some(context),
|
||||
Some(syn::parse_quote!(::dioxus_fullstack::prelude::server_fn)),
|
||||
quote::quote!(#args),
|
||||
mapped_body,
|
||||
trait_obj_wrapper,
|
||||
None,
|
||||
Some(server_fn_path.clone()),
|
||||
) {
|
||||
Err(e) => e.to_compile_error().into(),
|
||||
Ok(s) => s.to_token_stream().into(),
|
||||
Ok(tokens) => quote::quote! {
|
||||
#tokens
|
||||
#[cfg(feature = "ssr")]
|
||||
#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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -55,19 +55,17 @@
|
||||
//! ```
|
||||
|
||||
use axum::{
|
||||
body::{self, Body, BoxBody, Full},
|
||||
extract::{State, WebSocketUpgrade},
|
||||
body::{self, Body, BoxBody},
|
||||
extract::State,
|
||||
handler::Handler,
|
||||
http::{Request, Response, StatusCode},
|
||||
response::IntoResponse,
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use dioxus_core::VirtualDom;
|
||||
use server_fn::{Encoding, Payload, ServerFunctionRegistry};
|
||||
use std::error::Error;
|
||||
use server_fn::{Encoding, ServerFunctionRegistry};
|
||||
use std::sync::Arc;
|
||||
use tokio::task::spawn_blocking;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use crate::{
|
||||
prelude::*, render::SSRState, serve_config::ServeConfig, server_context::DioxusServerContext,
|
||||
@@ -92,7 +90,7 @@ pub trait DioxusRouterExt<S> {
|
||||
/// .register_server_fns_with_handler("", |func| {
|
||||
/// move |req: Request<Body>| async move {
|
||||
/// let (parts, body) = req.into_parts();
|
||||
/// let parts: Arc<RequestParts> = Arc::new(parts.into());
|
||||
/// 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
|
||||
/// }
|
||||
@@ -106,7 +104,7 @@ pub trait DioxusRouterExt<S> {
|
||||
fn register_server_fns_with_handler<H, T>(
|
||||
self,
|
||||
server_fn_route: &'static str,
|
||||
handler: impl FnMut(server_fn::ServerFnTraitObj<DioxusServerContext>) -> H,
|
||||
handler: impl FnMut(server_fn::ServerFnTraitObj<()>) -> H,
|
||||
) -> Self
|
||||
where
|
||||
H: Handler<T, S>,
|
||||
@@ -231,7 +229,7 @@ where
|
||||
fn register_server_fns_with_handler<H, T>(
|
||||
self,
|
||||
server_fn_route: &'static str,
|
||||
mut handler: impl FnMut(server_fn::ServerFnTraitObj<DioxusServerContext>) -> H,
|
||||
mut handler: impl FnMut(server_fn::ServerFnTraitObj<()>) -> H,
|
||||
) -> Self
|
||||
where
|
||||
H: Handler<T, S, Body>,
|
||||
@@ -256,11 +254,22 @@ where
|
||||
|
||||
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>| async move {
|
||||
let (parts, body) = req.into_parts();
|
||||
let parts: Arc<RequestParts> = Arc::new(parts.into());
|
||||
let server_context = DioxusServerContext::new(parts.clone());
|
||||
server_fn_handler(server_context, func.clone(), parts, body).await
|
||||
use crate::layer::Service;
|
||||
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::empty());
|
||||
*res.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -311,15 +320,13 @@ where
|
||||
cfg: impl Into<ServeConfig<P>>,
|
||||
) -> Self {
|
||||
let cfg = cfg.into();
|
||||
let ssr_state = SSRState::new(&cfg);
|
||||
|
||||
// Add server functions and render index.html
|
||||
self.serve_static_assets(cfg.assets_path)
|
||||
.route(
|
||||
"/",
|
||||
get(render_handler).with_state((cfg, SSRState::default())),
|
||||
)
|
||||
.connect_hot_reload()
|
||||
.register_server_fns(server_fn_route)
|
||||
.fallback(get(render_handler).with_state((cfg, ssr_state)))
|
||||
}
|
||||
|
||||
fn connect_hot_reload(self) -> Self {
|
||||
@@ -330,7 +337,7 @@ where
|
||||
Router::new()
|
||||
.route(
|
||||
"/disconnect",
|
||||
get(|ws: WebSocketUpgrade| async {
|
||||
get(|ws: axum::extract::WebSocketUpgrade| async {
|
||||
ws.on_upgrade(|mut ws| async move {
|
||||
use axum::extract::ws::Message;
|
||||
let _ = ws.send(Message::Text("connected".into())).await;
|
||||
@@ -352,96 +359,42 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_request_parts_to_response<B>(
|
||||
headers: hyper::header::HeaderMap,
|
||||
response: &mut axum::response::Response<B>,
|
||||
) {
|
||||
let mut_headers = response.headers_mut();
|
||||
for (key, value) in headers.iter() {
|
||||
mut_headers.insert(key, value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
async fn render_handler<P: Clone + serde::Serialize + Send + Sync + 'static>(
|
||||
State((cfg, ssr_state)): State<(ServeConfig<P>, SSRState)>,
|
||||
request: Request<Body>,
|
||||
) -> impl IntoResponse {
|
||||
let (parts, _) = request.into_parts();
|
||||
let parts: Arc<RequestParts> = Arc::new(parts.into());
|
||||
let server_context = DioxusServerContext::new(parts);
|
||||
let mut vdom =
|
||||
VirtualDom::new_with_props(cfg.app, cfg.props.clone()).with_root_context(server_context);
|
||||
let _ = vdom.rebuild();
|
||||
let url = parts.uri.path_and_query().unwrap().to_string();
|
||||
let parts: Arc<RwLock<http::request::Parts>> = Arc::new(RwLock::new(parts.into()));
|
||||
let server_context = DioxusServerContext::new(parts.clone());
|
||||
|
||||
let rendered = ssr_state.render_vdom(&vdom, &cfg);
|
||||
Full::from(rendered)
|
||||
}
|
||||
|
||||
/// A default handler for server functions. It will deserialize the request, call the server function, and serialize the response.
|
||||
pub async fn server_fn_handler(
|
||||
server_context: DioxusServerContext,
|
||||
function: server_fn::ServerFnTraitObj<DioxusServerContext>,
|
||||
parts: Arc<RequestParts>,
|
||||
body: Body,
|
||||
) -> impl IntoResponse {
|
||||
let body = hyper::body::to_bytes(body).await;
|
||||
let Ok(body) = body else {
|
||||
return report_err(body.err().unwrap());
|
||||
};
|
||||
|
||||
// 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 (resp_tx, resp_rx) = tokio::sync::oneshot::channel();
|
||||
let query_string = parts.uri.query().unwrap_or_default().to_string();
|
||||
spawn_blocking({
|
||||
move || {
|
||||
tokio::runtime::Runtime::new()
|
||||
.expect("couldn't spawn runtime")
|
||||
.block_on(async {
|
||||
let query = &query_string.into();
|
||||
let data = match &function.encoding() {
|
||||
Encoding::Url | Encoding::Cbor => &body,
|
||||
Encoding::GetJSON | Encoding::GetCBOR => query,
|
||||
};
|
||||
let resp = match function.call(server_context.clone(), data).await {
|
||||
Ok(serialized) => {
|
||||
// if this is Accept: application/json then send a serialized JSON response
|
||||
let accept_header = parts
|
||||
.headers
|
||||
.get("Accept")
|
||||
.and_then(|value| value.to_str().ok());
|
||||
let mut res = Response::builder();
|
||||
*res.headers_mut().expect("empty response should be valid") =
|
||||
server_context.take_response_headers();
|
||||
if accept_header == Some("application/json")
|
||||
|| accept_header
|
||||
== Some(
|
||||
"application/\
|
||||
x-www-form-urlencoded",
|
||||
)
|
||||
|| accept_header == Some("application/cbor")
|
||||
{
|
||||
res = res.status(StatusCode::OK);
|
||||
}
|
||||
|
||||
let resp = match serialized {
|
||||
Payload::Binary(data) => res
|
||||
.header("Content-Type", "application/cbor")
|
||||
.body(body::boxed(Full::from(data))),
|
||||
Payload::Url(data) => res
|
||||
.header(
|
||||
"Content-Type",
|
||||
"application/\
|
||||
x-www-form-urlencoded",
|
||||
)
|
||||
.body(body::boxed(data)),
|
||||
Payload::Json(data) => res
|
||||
.header("Content-Type", "application/json")
|
||||
.body(body::boxed(data)),
|
||||
};
|
||||
|
||||
resp.unwrap()
|
||||
}
|
||||
Err(e) => report_err(e),
|
||||
};
|
||||
|
||||
resp_tx.send(resp).unwrap();
|
||||
})
|
||||
match ssr_state.render(url, &cfg, &server_context).await {
|
||||
Ok(rendered) => {
|
||||
let crate::render::RenderResponse { html, freshness } = rendered;
|
||||
let mut response = axum::response::Html::from(html).into_response();
|
||||
freshness.write(response.headers_mut());
|
||||
let headers = server_context.response_parts().unwrap().headers.clone();
|
||||
apply_request_parts_to_response(headers, &mut response);
|
||||
response
|
||||
}
|
||||
});
|
||||
resp_rx.await.unwrap()
|
||||
Err(e) => {
|
||||
log::error!("Failed to render page: {}", e);
|
||||
report_err(e).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn report_err<E: Error>(e: E) -> Response<BoxBody> {
|
||||
fn report_err<E: std::fmt::Display>(e: E) -> Response<BoxBody> {
|
||||
Response::builder()
|
||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.body(body::boxed(format!("Error: {}", e)))
|
||||
@@ -450,7 +403,7 @@ fn report_err<E: Error>(e: E) -> Response<BoxBody> {
|
||||
|
||||
/// 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", feature = "ssr"))]
|
||||
pub async fn hot_reload_handler(ws: WebSocketUpgrade) -> impl IntoResponse {
|
||||
pub async fn hot_reload_handler(ws: axum::extract::WebSocketUpgrade) -> impl IntoResponse {
|
||||
use axum::extract::ws::Message;
|
||||
use futures_util::StreamExt;
|
||||
|
||||
|
||||
@@ -16,3 +16,145 @@ pub mod axum_adapter;
|
||||
pub mod salvo_adapter;
|
||||
#[cfg(feature = "warp")]
|
||||
pub mod warp_adapter;
|
||||
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use http::StatusCode;
|
||||
|
||||
use server_fn::{Encoding, Payload};
|
||||
use tokio::task::spawn_blocking;
|
||||
|
||||
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();
|
||||
Box::pin(async move {
|
||||
let query = req.uri().query().unwrap_or_default().as_bytes().to_vec();
|
||||
let (parts, body) = req.into_parts();
|
||||
let body = hyper::body::to_bytes(body).await?.to_vec();
|
||||
let 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 (resp_tx, resp_rx) = tokio::sync::oneshot::channel();
|
||||
spawn_blocking({
|
||||
let function = function.clone();
|
||||
let mut server_context = server_context.clone();
|
||||
server_context.parts = parts;
|
||||
move || {
|
||||
tokio::runtime::Runtime::new()
|
||||
.expect("couldn't spawn runtime")
|
||||
.block_on(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(),
|
||||
);
|
||||
let resp = server_function_future.await;
|
||||
|
||||
resp_tx.send(resp).unwrap();
|
||||
})
|
||||
}
|
||||
});
|
||||
let result = resp_rx.await.unwrap();
|
||||
let mut res = http::Response::builder();
|
||||
|
||||
// Set the headers from the server context
|
||||
let parts = server_context.parts.read().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())?
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -49,22 +49,31 @@
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
use dioxus_core::VirtualDom;
|
||||
use hyper::{http::HeaderValue, StatusCode};
|
||||
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, FlowCtrl, Handler, Request, Response, Router,
|
||||
Depot, Error as SalvoError, FlowCtrl, Handler, Request, Response, Router,
|
||||
};
|
||||
use server_fn::{Encoding, Payload, ServerFunctionRegistry};
|
||||
use server_fn::{Encoding, ServerFunctionRegistry};
|
||||
use std::error::Error;
|
||||
use std::sync::Arc;
|
||||
use tokio::task::spawn_blocking;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use crate::{
|
||||
prelude::*, render::SSRState, serve_config::ServeConfig, server_fn::DioxusServerFnRegistry,
|
||||
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.
|
||||
@@ -76,7 +85,7 @@ pub trait DioxusRouterExt {
|
||||
/// use dioxus_fullstack::prelude::*;
|
||||
///
|
||||
/// struct ServerFunctionHandler {
|
||||
/// server_fn: server_fn::ServerFnTraitObj<DioxusServerContext>,
|
||||
/// server_fn: server_fn::ServerFnTraitObj<()>,
|
||||
/// }
|
||||
///
|
||||
/// #[handler]
|
||||
@@ -109,7 +118,7 @@ pub trait DioxusRouterExt {
|
||||
fn register_server_fns_with_handler<H>(
|
||||
self,
|
||||
server_fn_route: &'static str,
|
||||
handler: impl Fn(server_fn::ServerFnTraitObj<DioxusServerContext>) -> H,
|
||||
handler: impl Fn(server_fn::ServerFnTraitObj<()>) -> H,
|
||||
) -> Self
|
||||
where
|
||||
H: Handler + 'static;
|
||||
@@ -203,7 +212,7 @@ impl DioxusRouterExt for Router {
|
||||
fn register_server_fns_with_handler<H>(
|
||||
self,
|
||||
server_fn_route: &'static str,
|
||||
mut handler: impl FnMut(server_fn::ServerFnTraitObj<DioxusServerContext>) -> H,
|
||||
mut handler: impl FnMut(server_fn::ServerFnTraitObj<()>) -> H,
|
||||
) -> Self
|
||||
where
|
||||
H: Handler + 'static,
|
||||
@@ -282,7 +291,7 @@ impl DioxusRouterExt for Router {
|
||||
self.serve_static_assets(cfg.assets_path)
|
||||
.connect_hot_reload()
|
||||
.register_server_fns(server_fn_path)
|
||||
.push(Router::with_path("/").get(SSRHandler { cfg }))
|
||||
.push(Router::with_path("/<**any_path>").get(SSRHandler { cfg }))
|
||||
}
|
||||
|
||||
fn connect_hot_reload(self) -> Self {
|
||||
@@ -298,13 +307,71 @@ impl DioxusRouterExt for 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) -> RequestParts {
|
||||
RequestParts {
|
||||
method: std::mem::take(req.method_mut()),
|
||||
uri: std::mem::take(req.uri_mut()),
|
||||
version: req.version(),
|
||||
headers: std::mem::take(req.headers_mut()),
|
||||
extensions: std::mem::take(req.extensions_mut()),
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,34 +392,47 @@ impl<P: Clone + serde::Serialize + Send + Sync + 'static> Handler for SSRHandler
|
||||
let renderer_pool = if let Some(renderer) = depot.obtain::<SSRState>() {
|
||||
renderer.clone()
|
||||
} else {
|
||||
let renderer = SSRState::default();
|
||||
let renderer = SSRState::new(&self.cfg);
|
||||
depot.inject(renderer.clone());
|
||||
renderer
|
||||
};
|
||||
let parts: Arc<RequestParts> = Arc::new(extract_parts(req));
|
||||
|
||||
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);
|
||||
let mut vdom = VirtualDom::new_with_props(self.cfg.app, self.cfg.props.clone())
|
||||
.with_root_context(server_context.clone());
|
||||
let _ = vdom.rebuild();
|
||||
|
||||
res.write_body(renderer_pool.render_vdom(&vdom, &self.cfg))
|
||||
.unwrap();
|
||||
match renderer_pool
|
||||
.render(route, &self.cfg, &server_context)
|
||||
.await
|
||||
{
|
||||
Ok(rendered) => {
|
||||
let crate::render::RenderResponse { html, freshness } = rendered;
|
||||
|
||||
*res.headers_mut() = server_context.take_response_headers();
|
||||
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) => {
|
||||
log::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<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<DioxusServerContext>,
|
||||
function: server_fn::ServerFnTraitObj<()>,
|
||||
) -> Self {
|
||||
let server_context = server_context.into();
|
||||
Self {
|
||||
@@ -365,90 +445,14 @@ impl ServerFnHandler {
|
||||
#[handler]
|
||||
impl ServerFnHandler {
|
||||
async fn handle(&self, req: &mut Request, _depot: &mut Depot, res: &mut Response) {
|
||||
let Self {
|
||||
server_context,
|
||||
function,
|
||||
} = self;
|
||||
|
||||
let query = req
|
||||
.uri()
|
||||
.query()
|
||||
.unwrap_or_default()
|
||||
.as_bytes()
|
||||
.to_vec()
|
||||
.into();
|
||||
let body = hyper::body::to_bytes(req.body_mut().unwrap()).await;
|
||||
let Ok(body)=body else {
|
||||
handle_error(body.err().unwrap(), res);
|
||||
return;
|
||||
};
|
||||
let headers = req.headers();
|
||||
let accept_header = headers.get("Accept").cloned();
|
||||
|
||||
let parts = Arc::new(extract_parts(req));
|
||||
|
||||
// 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 (resp_tx, resp_rx) = tokio::sync::oneshot::channel();
|
||||
spawn_blocking({
|
||||
let function = function.clone();
|
||||
let mut server_context = server_context.clone();
|
||||
server_context.parts = parts;
|
||||
move || {
|
||||
tokio::runtime::Runtime::new()
|
||||
.expect("couldn't spawn runtime")
|
||||
.block_on(async move {
|
||||
let data = match function.encoding() {
|
||||
Encoding::Url | Encoding::Cbor => &body,
|
||||
Encoding::GetJSON | Encoding::GetCBOR => &query,
|
||||
};
|
||||
let resp = function.call(server_context, data).await;
|
||||
|
||||
resp_tx.send(resp).unwrap();
|
||||
})
|
||||
}
|
||||
});
|
||||
let result = resp_rx.await.unwrap();
|
||||
|
||||
// Set the headers from the server context
|
||||
*res.headers_mut() = server_context.take_response_headers();
|
||||
|
||||
match result {
|
||||
Ok(serialized) => {
|
||||
// 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.set_status_code(StatusCode::OK);
|
||||
}
|
||||
|
||||
match serialized {
|
||||
Payload::Binary(data) => {
|
||||
res.headers_mut()
|
||||
.insert("Content-Type", HeaderValue::from_static("application/cbor"));
|
||||
res.write_body(data).unwrap();
|
||||
}
|
||||
Payload::Url(data) => {
|
||||
res.headers_mut().insert(
|
||||
"Content-Type",
|
||||
HeaderValue::from_static(
|
||||
"application/\
|
||||
x-www-form-urlencoded",
|
||||
),
|
||||
);
|
||||
res.write_body(data).unwrap();
|
||||
}
|
||||
Payload::Json(data) => {
|
||||
res.headers_mut()
|
||||
.insert("Content-Type", HeaderValue::from_static("application/json"));
|
||||
res.write_body(data).unwrap();
|
||||
}
|
||||
}
|
||||
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),
|
||||
}
|
||||
@@ -457,7 +461,7 @@ impl ServerFnHandler {
|
||||
|
||||
fn handle_error(error: impl Error + Send + Sync, res: &mut Response) {
|
||||
let mut resp_err = Response::new();
|
||||
resp_err.set_status_code(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
resp_err.status_code(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
resp_err.render(format!("Internal Server Error: {}", error));
|
||||
*res = resp_err;
|
||||
}
|
||||
@@ -494,8 +498,8 @@ impl HotReloadHandler {
|
||||
_depot: &mut Depot,
|
||||
res: &mut Response,
|
||||
) -> Result<(), salvo::http::StatusError> {
|
||||
use salvo::ws::Message;
|
||||
use salvo::ws::WebSocketUpgrade;
|
||||
use salvo::websocket::Message;
|
||||
use salvo::websocket::WebSocketUpgrade;
|
||||
|
||||
let state = crate::hot_reload::spawn_hot_reload().await;
|
||||
|
||||
@@ -542,10 +546,10 @@ impl HotReloadHandler {
|
||||
#[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))]
|
||||
#[handler]
|
||||
async fn ignore_ws(req: &mut Request, res: &mut Response) -> Result<(), salvo::http::StatusError> {
|
||||
use salvo::ws::WebSocketUpgrade;
|
||||
use salvo::websocket::WebSocketUpgrade;
|
||||
WebSocketUpgrade::new()
|
||||
.upgrade(req, res, |mut ws| async move {
|
||||
let _ = ws.send(salvo::ws::Message::text("connected")).await;
|
||||
let _ = ws.send(salvo::websocket::Message::text("connected")).await;
|
||||
while let Some(msg) = ws.recv().await {
|
||||
if msg.is_err() {
|
||||
return;
|
||||
|
||||
@@ -46,16 +46,19 @@
|
||||
//!
|
||||
//! ```
|
||||
|
||||
use crate::layer::Service;
|
||||
use crate::{
|
||||
prelude::*, render::SSRState, serve_config::ServeConfig, server_fn::DioxusServerFnRegistry,
|
||||
};
|
||||
|
||||
use dioxus_core::VirtualDom;
|
||||
use crate::server_fn_service;
|
||||
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},
|
||||
@@ -71,19 +74,22 @@ use warp::{
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// let routes = register_server_fns_with_handler("", |full_route, func| {
|
||||
/// let routes = register_server_fns_with_handler(server_fn_route, |full_route, func| {
|
||||
/// path(full_route)
|
||||
/// .and(post())
|
||||
/// .and(header::headers_cloned())
|
||||
/// .and(body::bytes())
|
||||
/// .and_then(move |headers: HeaderMap, body| {
|
||||
/// let func = func.clone();
|
||||
/// async move {
|
||||
/// // Add the headers to the server function context
|
||||
/// server_fn_handler((headers.clone(),), func, headers, body).await
|
||||
/// }
|
||||
/// })
|
||||
/// });
|
||||
/// .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| {
|
||||
/// log::error!("Server function error: {}", err);
|
||||
/// warp::reject::reject()
|
||||
/// })
|
||||
/// }
|
||||
/// })
|
||||
/// })
|
||||
/// warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
|
||||
/// }
|
||||
/// ```
|
||||
@@ -92,7 +98,7 @@ pub fn register_server_fns_with_handler<H, F, R>(
|
||||
mut handler: H,
|
||||
) -> BoxedFilter<(R,)>
|
||||
where
|
||||
H: FnMut(String, server_fn::ServerFnTraitObj<DioxusServerContext>) -> F,
|
||||
H: FnMut(String, server_fn::ServerFnTraitObj<()>) -> F,
|
||||
F: Filter<Extract = (R,), Error = warp::Rejection> + Send + Sync + 'static,
|
||||
F::Extract: Send,
|
||||
R: Reply + 'static,
|
||||
@@ -131,10 +137,14 @@ pub fn register_server_fns(server_fn_route: &'static str) -> BoxedFilter<(impl R
|
||||
.and(warp::post().or(warp::get()).unify())
|
||||
.and(request_parts())
|
||||
.and(warp::body::bytes())
|
||||
.and_then(move |parts, bytes| {
|
||||
let func = func.clone();
|
||||
.and_then(move |parts, bytes: bytes::Bytes| {
|
||||
let mut service = server_fn_service(DioxusServerContext::default(), func.clone());
|
||||
async move {
|
||||
server_fn_handler(DioxusServerContext::default(), func, parts, bytes).await
|
||||
let req = warp::hyper::Request::from_parts(parts, bytes.into());
|
||||
service.run(req).await.map_err(|err| {
|
||||
log::error!("Server function error: {}", err);
|
||||
warp::reject::reject()
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -168,9 +178,14 @@ pub fn serve_dioxus_application<P: Clone + serde::Serialize + Send + Sync + 'sta
|
||||
let serve_dir = warp::fs::dir(cfg.assets_path);
|
||||
|
||||
connect_hot_reload()
|
||||
// First register the server functions
|
||||
.or(register_server_fns(server_fn_route))
|
||||
.or(warp::path::end().and(render_ssr(cfg)))
|
||||
// Then the index route
|
||||
.or(path::end().and(render_ssr(cfg.clone())))
|
||||
// Then the static assets
|
||||
.or(serve_dir)
|
||||
// Then all other routes
|
||||
.or(render_ssr(cfg))
|
||||
.boxed()
|
||||
}
|
||||
|
||||
@@ -180,32 +195,47 @@ pub fn render_ssr<P: Clone + serde::Serialize + Send + Sync + 'static>(
|
||||
) -> impl Filter<Extract = (impl Reply,), Error = warp::Rejection> + Clone {
|
||||
warp::get()
|
||||
.and(request_parts())
|
||||
.and(with_ssr_state())
|
||||
.map(move |parts, renderer: SSRState| {
|
||||
let parts = Arc::new(parts);
|
||||
.and(with_ssr_state(&cfg))
|
||||
.then(move |parts: http::request::Parts, renderer: SSRState| {
|
||||
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);
|
||||
|
||||
let server_context = DioxusServerContext::new(parts);
|
||||
match renderer.render(route, &cfg, &server_context).await {
|
||||
Ok(rendered) => {
|
||||
let crate::render::RenderResponse { html, freshness } = rendered;
|
||||
|
||||
let mut vdom = VirtualDom::new_with_props(cfg.app, cfg.props.clone())
|
||||
.with_root_context(server_context.clone());
|
||||
let _ = vdom.rebuild();
|
||||
let mut res = Response::builder()
|
||||
.header("Content-Type", "text/html")
|
||||
.body(html)
|
||||
.unwrap();
|
||||
|
||||
let html = renderer.render_vdom(&vdom, &cfg);
|
||||
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);
|
||||
|
||||
let mut res = Response::builder();
|
||||
|
||||
*res.headers_mut().expect("empty request should be valid") =
|
||||
server_context.take_response_headers();
|
||||
|
||||
res.header("Content-Type", "text/html")
|
||||
.body(Bytes::from(html))
|
||||
.unwrap()
|
||||
res
|
||||
}
|
||||
Err(err) => {
|
||||
log::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 = (RequestParts,), Error = warp::reject::Rejection> + Clone {
|
||||
) -> impl Filter<Extract = (http::request::Parts,), Error = warp::reject::Rejection> + Clone {
|
||||
warp::method()
|
||||
.and(warp::filters::path::full())
|
||||
.and(
|
||||
@@ -221,19 +251,23 @@ pub fn request_parts(
|
||||
.map_err(|err| {
|
||||
warp::reject::custom(FailedToReadBody(format!("Failed to build uri: {}", err)))
|
||||
})
|
||||
.map(|uri| RequestParts {
|
||||
method,
|
||||
uri,
|
||||
headers,
|
||||
..Default::default()
|
||||
.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() -> impl Filter<Extract = (SSRState,), Error = std::convert::Infallible> + Clone
|
||||
{
|
||||
let state = SSRState::default();
|
||||
warp::any().map(move || state.clone())
|
||||
fn with_ssr_state<P: Clone + serde::Serialize + Send + Sync + 'static>(
|
||||
cfg: &ServeConfig<P>,
|
||||
) -> impl Filter<Extract = (SSRState,), Error = std::convert::Infallible> + Clone {
|
||||
let renderer = SSRState::new(cfg);
|
||||
warp::any().map(move || renderer.clone())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -246,102 +280,6 @@ struct RecieveFailed(String);
|
||||
|
||||
impl warp::reject::Reject for RecieveFailed {}
|
||||
|
||||
/// A default handler for server functions. It will deserialize the request body, call the server function, and serialize the response.
|
||||
pub async fn server_fn_handler(
|
||||
server_context: impl Into<DioxusServerContext>,
|
||||
function: server_fn::ServerFnTraitObj<DioxusServerContext>,
|
||||
parts: RequestParts,
|
||||
body: Bytes,
|
||||
) -> Result<Box<dyn warp::Reply>, warp::Rejection> {
|
||||
let mut server_context = server_context.into();
|
||||
|
||||
let parts = Arc::new(parts);
|
||||
|
||||
server_context.parts = parts.clone();
|
||||
|
||||
// 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 (resp_tx, resp_rx) = tokio::sync::oneshot::channel();
|
||||
spawn_blocking({
|
||||
move || {
|
||||
tokio::runtime::Runtime::new()
|
||||
.expect("couldn't spawn runtime")
|
||||
.block_on(async move {
|
||||
let query = parts
|
||||
.uri
|
||||
.query()
|
||||
.unwrap_or_default()
|
||||
.as_bytes()
|
||||
.to_vec()
|
||||
.into();
|
||||
let data = match function.encoding() {
|
||||
Encoding::Url | Encoding::Cbor => &body,
|
||||
Encoding::GetJSON | Encoding::GetCBOR => &query,
|
||||
};
|
||||
let resp = match function.call(server_context.clone(), data).await {
|
||||
Ok(serialized) => {
|
||||
// if this is Accept: application/json then send a serialized JSON response
|
||||
let accept_header = parts
|
||||
.headers
|
||||
.get("Accept")
|
||||
.as_ref()
|
||||
.and_then(|value| value.to_str().ok());
|
||||
let mut res = Response::builder();
|
||||
|
||||
*res.headers_mut().expect("empty request should be valid") =
|
||||
server_context.take_response_headers();
|
||||
|
||||
if accept_header == Some("application/json")
|
||||
|| accept_header
|
||||
== Some(
|
||||
"application/\
|
||||
x-www-form-urlencoded",
|
||||
)
|
||||
|| accept_header == Some("application/cbor")
|
||||
{
|
||||
res = res.status(StatusCode::OK);
|
||||
}
|
||||
|
||||
let resp = match serialized {
|
||||
Payload::Binary(data) => res
|
||||
.header("Content-Type", "application/cbor")
|
||||
.body(Bytes::from(data)),
|
||||
Payload::Url(data) => res
|
||||
.header(
|
||||
"Content-Type",
|
||||
"application/\
|
||||
x-www-form-urlencoded",
|
||||
)
|
||||
.body(Bytes::from(data)),
|
||||
Payload::Json(data) => res
|
||||
.header("Content-Type", "application/json")
|
||||
.body(Bytes::from(data)),
|
||||
};
|
||||
|
||||
Box::new(resp.unwrap())
|
||||
}
|
||||
Err(e) => report_err(e),
|
||||
};
|
||||
|
||||
if resp_tx.send(resp).is_err() {
|
||||
eprintln!("Error sending response");
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
resp_rx.await.map_err(|err| {
|
||||
warp::reject::custom(RecieveFailed(format!("Failed to recieve response {err}")))
|
||||
})
|
||||
}
|
||||
|
||||
fn report_err<E: Error>(e: E) -> Box<dyn warp::Reply> {
|
||||
Box::new(
|
||||
Response::builder()
|
||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.body(format!("Error: {}", e))
|
||||
.unwrap(),
|
||||
) as Box<dyn warp::Reply>
|
||||
}
|
||||
|
||||
/// 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
|
||||
@@ -360,9 +298,8 @@ pub fn connect_hot_reload() -> impl Filter<Extract = (impl Reply,), Error = warp
|
||||
#[cfg(not(all(debug_assertions, feature = "hot-reload", feature = "ssr")))]
|
||||
{
|
||||
warp::path!("_dioxus" / "hot_reload")
|
||||
.and(warp::ws())
|
||||
.map(warp::reply)
|
||||
.map(|reply| warp::reply::with_status(reply, warp::http::StatusCode::NOT_FOUND));
|
||||
.map(|reply| warp::reply::with_status(reply, warp::http::StatusCode::NOT_FOUND))
|
||||
}
|
||||
#[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))]
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use dioxus_core::Template;
|
||||
use dioxus::prelude::Template;
|
||||
use tokio::sync::{
|
||||
watch::{channel, Receiver},
|
||||
RwLock,
|
||||
@@ -9,7 +9,8 @@ use tokio::sync::{
|
||||
#[derive(Clone)]
|
||||
pub struct HotReloadState {
|
||||
// The cache of all templates that have been modified since the last time we checked
|
||||
pub(crate) templates: Arc<RwLock<std::collections::HashSet<dioxus_core::Template<'static>>>>,
|
||||
pub(crate) templates:
|
||||
Arc<RwLock<std::collections::HashSet<dioxus::prelude::Template<'static>>>>,
|
||||
// The channel to send messages to the hot reload thread
|
||||
pub(crate) message_receiver: Receiver<Option<Template<'static>>>,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
//! Launch helper macros for fullstack apps
|
||||
#![allow(unused)]
|
||||
|
||||
#[macro_export]
|
||||
/// Launch a server with a router
|
||||
macro_rules! launch_router {
|
||||
(@router_config) => {
|
||||
dioxus_fullstack::router::FullstackRouterConfig::default()
|
||||
};
|
||||
|
||||
(@router_config $router_cfg:expr) => {
|
||||
$router_cfg
|
||||
};
|
||||
|
||||
(@$address:expr, $route:ty, $(cfg: $router_cfg:expr,)? {$($rule:ident $(: $cfg:expr)?,)*}) => {
|
||||
dioxus_fullstack::launch!(
|
||||
@$address,
|
||||
dioxus_fullstack::router::RouteWithCfg::<$route>,
|
||||
(dioxus_fullstack::launch_router!(@router_config $($router_cfg)?)),
|
||||
{
|
||||
$($rule $(: $cfg)?,)*
|
||||
}
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
/// Launch a server
|
||||
macro_rules! launch {
|
||||
(@web_cfg $server_cfg:ident $wcfg:expr) => {
|
||||
#[cfg(feature = "web")]
|
||||
let web_cfg = $wcfg;
|
||||
};
|
||||
|
||||
(@web_cfg $server_cfg:ident) => {
|
||||
#[cfg(feature = "web")]
|
||||
let web_cfg = dioxus_web::Config::new();
|
||||
};
|
||||
|
||||
(@serve_cfg $server_cfg:ident $cfg:expr) => {
|
||||
#[cfg(feature = "ssr")]
|
||||
let $server_cfg = $cfg;
|
||||
};
|
||||
|
||||
(@hot_reload $server_cfg:ident) => {
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
dioxus_fullstack::prelude::hot_reload_init!(dioxus_hot_reload::Config::new().with_rebuild_callback(|| {
|
||||
std::process::Command::new("cargo")
|
||||
.args(&["run", "--features", "ssr"])
|
||||
.spawn()
|
||||
.unwrap()
|
||||
.wait()
|
||||
.unwrap();
|
||||
std::process::Command::new("cargo")
|
||||
.args(&["run", "--features", "web"])
|
||||
.spawn()
|
||||
.unwrap()
|
||||
.wait()
|
||||
.unwrap();
|
||||
true
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
(@hot_reload $server_cfg:ident $hot_reload_cfg:expr) => {
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
dioxus_fullstack::prelude::hot_reload_init!($hot_reload_cfg);
|
||||
}
|
||||
};
|
||||
|
||||
(@incremental $server_cfg:ident) => {
|
||||
#[cfg(feature = "ssr")]
|
||||
let $server_cfg = $server_cfg.incremental(dioxus_fullstack::prelude::IncrementalRendererConfig::default());
|
||||
};
|
||||
|
||||
(@incremental $server_cfg:ident $cfg:expr) => {
|
||||
#[cfg(feature = "ssr")]
|
||||
let $server_cfg = $server_cfg.incremental($cfg);
|
||||
};
|
||||
|
||||
(@props_type) => {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
(@props_type $props:expr) => {
|
||||
$props
|
||||
};
|
||||
|
||||
(@ $address:expr, $comp:path, $(( $props:expr ),)? {$($rule:ident $(: $cfg:expr)?,)*}) => {
|
||||
#[cfg(feature = "web")]
|
||||
{
|
||||
#[allow(unused)]
|
||||
let web_cfg = dioxus_web::Config::new();
|
||||
|
||||
$(
|
||||
dioxus_fullstack::prelude::launch!(@$rule server_cfg $($cfg)?);
|
||||
)*
|
||||
|
||||
dioxus_web::launch_with_props(
|
||||
$comp,
|
||||
dioxus_fullstack::prelude::get_root_props_from_document().expect("Failed to get root props from document"),
|
||||
web_cfg.hydrate(true),
|
||||
);
|
||||
}
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
let server_cfg = dioxus_fullstack::prelude::ServeConfigBuilder::new($comp, dioxus_fullstack::prelude::launch!(@props_type $($props)?));
|
||||
|
||||
$(
|
||||
dioxus_fullstack::prelude::launch!(@$rule server_cfg $($cfg)?);
|
||||
)*
|
||||
|
||||
tokio::runtime::Runtime::new()
|
||||
.unwrap()
|
||||
.block_on(async move {
|
||||
let addr = std::net::SocketAddr::from($address);
|
||||
|
||||
dioxus_fullstack::launch::launch_server(addr, server_cfg.build()).await;
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Launch a server with the given configeration
|
||||
/// This will use the routing intigration of the currently enabled intigration feature
|
||||
#[cfg(feature = "ssr")]
|
||||
pub async fn launch_server<P: Clone + serde::Serialize + Send + Sync + 'static>(
|
||||
addr: std::net::SocketAddr,
|
||||
cfg: crate::prelude::ServeConfig<P>,
|
||||
) {
|
||||
#[cfg(all(feature = "axum", not(feature = "warp"), not(feature = "salvo")))]
|
||||
{
|
||||
use crate::adapters::axum_adapter::DioxusRouterExt;
|
||||
use tower::ServiceBuilder;
|
||||
|
||||
axum::Server::bind(&addr)
|
||||
.serve(
|
||||
axum::Router::new()
|
||||
.serve_dioxus_application("", cfg)
|
||||
.layer(
|
||||
ServiceBuilder::new()
|
||||
.layer(tower_http::compression::CompressionLayer::new().gzip(true)),
|
||||
)
|
||||
.into_make_service(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
#[cfg(all(feature = "warp", not(feature = "axum"), not(feature = "salvo")))]
|
||||
{
|
||||
use warp::Filter;
|
||||
warp::serve(
|
||||
crate::prelude::serve_dioxus_application("", cfg)
|
||||
.with(warp::filters::compression::gzip()),
|
||||
)
|
||||
.run(addr)
|
||||
.await;
|
||||
}
|
||||
#[cfg(all(feature = "salvo", not(feature = "axum"), not(feature = "warp")))]
|
||||
{
|
||||
use crate::adapters::salvo_adapter::DioxusRouterExt;
|
||||
use salvo::conn::Listener;
|
||||
let router = salvo::Router::new().serve_dioxus_application("", cfg).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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
use std::pin::Pin;
|
||||
|
||||
use http::{Request, Response};
|
||||
|
||||
pub trait Layer: Send + Sync + 'static {
|
||||
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)))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Service {
|
||||
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);
|
||||
Box::pin(async move { fut.await.map_err(|err| err.into()) })
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -3,17 +3,27 @@
|
||||
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
|
||||
#![deny(missing_docs)]
|
||||
|
||||
pub use adapters::*;
|
||||
pub use once_cell;
|
||||
|
||||
mod props_html;
|
||||
|
||||
#[cfg(feature = "router")]
|
||||
pub mod router;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
mod adapters;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub use adapters::*;
|
||||
#[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))]
|
||||
mod hot_reload;
|
||||
pub mod launch;
|
||||
#[cfg(feature = "ssr")]
|
||||
mod layer;
|
||||
#[cfg(feature = "ssr")]
|
||||
mod render;
|
||||
#[cfg(feature = "ssr")]
|
||||
mod serve_config;
|
||||
#[cfg(feature = "ssr")]
|
||||
mod server_context;
|
||||
mod server_fn;
|
||||
|
||||
@@ -27,16 +37,24 @@ pub mod prelude {
|
||||
pub use crate::adapters::warp_adapter::*;
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub use crate::props_html::deserialize_props::get_root_props_from_document;
|
||||
#[cfg(all(feature = "ssr", feature = "router"))]
|
||||
pub use crate::render::pre_cache_static_routes_with_props;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub use crate::render::SSRState;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub use crate::serve_config::{ServeConfig, ServeConfigBuilder};
|
||||
#[cfg(all(feature = "ssr", feature = "axum"))]
|
||||
pub use crate::server_context::Axum;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub use crate::server_context::RequestParts;
|
||||
pub use crate::server_context::{DioxusServerContext, HasServerContext};
|
||||
pub use crate::server_context::{
|
||||
extract, server_context, DioxusServerContext, FromServerContext, ProvideServerContext,
|
||||
};
|
||||
pub use crate::server_fn::DioxusServerFn;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub use crate::server_fn::{ServerFnTraitObj, ServerFunction};
|
||||
pub use crate::server_fn::{ServerFnMiddleware, ServerFnTraitObj, ServerFunction};
|
||||
pub use crate::{launch, launch_router};
|
||||
pub use dioxus_server_macro::*;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub use dioxus_ssr::incremental::IncrementalRendererConfig;
|
||||
pub use server_fn::{self, ServerFn as _, ServerFnError};
|
||||
}
|
||||
@@ -4,9 +4,8 @@ use base64::engine::general_purpose::STANDARD;
|
||||
use base64::Engine;
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) fn serde_from_string<T: DeserializeOwned>(string: &str) -> Option<T> {
|
||||
let decompressed = STANDARD.decode(string.as_bytes()).ok()?;
|
||||
let (decompressed, _) = yazi::decompress(&decompressed, yazi::Format::Zlib).unwrap();
|
||||
pub(crate) fn serde_from_bytes<T: DeserializeOwned>(string: &[u8]) -> Option<T> {
|
||||
let decompressed = STANDARD.decode(string).ok()?;
|
||||
|
||||
postcard::from_bytes(&decompressed).ok()
|
||||
}
|
||||
@@ -27,6 +26,6 @@ pub fn get_root_props_from_document<T: DeserializeOwned>() -> Option<T> {
|
||||
.get_element_by_id("dioxus-storage")?
|
||||
.get_attribute("data-serialized")?;
|
||||
|
||||
serde_from_string(&attribute)
|
||||
serde_from_bytes(attribute.as_bytes())
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ fn serialized_and_deserializes() {
|
||||
|
||||
for x in 0..10usize {
|
||||
for y in 0..10 {
|
||||
let mut as_string = String::new();
|
||||
let mut as_string: Vec<u8> = Vec::new();
|
||||
let data = vec![
|
||||
Data {
|
||||
a: x as u32,
|
||||
@@ -39,7 +39,7 @@ fn serialized_and_deserializes() {
|
||||
];
|
||||
serialize_props::serde_to_writable(&data, &mut as_string).unwrap();
|
||||
|
||||
println!("{}", as_string);
|
||||
println!("{:?}", as_string);
|
||||
println!(
|
||||
"original size: {}",
|
||||
std::mem::size_of::<Data>() * data.len()
|
||||
@@ -47,7 +47,7 @@ fn serialized_and_deserializes() {
|
||||
println!("serialized size: {}", to_allocvec(&data).unwrap().len());
|
||||
println!("compressed size: {}", as_string.len());
|
||||
|
||||
let decoded: Vec<Data> = deserialize_props::serde_from_string(&as_string).unwrap();
|
||||
let decoded: Vec<Data> = deserialize_props::serde_from_bytes(&as_string).unwrap();
|
||||
assert_eq!(data, decoded);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,16 +6,10 @@ use base64::Engine;
|
||||
#[allow(unused)]
|
||||
pub(crate) fn serde_to_writable<T: Serialize>(
|
||||
value: &T,
|
||||
mut write_to: impl std::fmt::Write,
|
||||
) -> std::fmt::Result {
|
||||
write_to: &mut impl std::io::Write,
|
||||
) -> std::io::Result<()> {
|
||||
let serialized = postcard::to_allocvec(value).unwrap();
|
||||
let compressed = yazi::compress(
|
||||
&serialized,
|
||||
yazi::Format::Zlib,
|
||||
yazi::CompressionLevel::BestSize,
|
||||
)
|
||||
.unwrap();
|
||||
write_to.write_str(&STANDARD.encode(compressed));
|
||||
write_to.write_all(STANDARD.encode(serialized).as_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -23,9 +17,10 @@ pub(crate) fn serde_to_writable<T: Serialize>(
|
||||
/// Encode data into a element. This is inteded to be used in the server to send data to the client.
|
||||
pub(crate) fn encode_in_element<T: Serialize>(
|
||||
data: T,
|
||||
mut write_to: impl std::fmt::Write,
|
||||
) -> std::fmt::Result {
|
||||
write_to.write_str(r#"<meta hidden="true" id="dioxus-storage" data-serialized=""#)?;
|
||||
serde_to_writable(&data, &mut write_to)?;
|
||||
write_to.write_str(r#"" />"#)
|
||||
write_to: &mut impl std::io::Write,
|
||||
) -> std::io::Result<()> {
|
||||
write_to
|
||||
.write_all(r#"<meta hidden="true" id="dioxus-storage" data-serialized=""#.as_bytes())?;
|
||||
serde_to_writable(&data, write_to)?;
|
||||
write_to.write_all(r#"" />"#.as_bytes())
|
||||
}
|
||||
@@ -2,56 +2,152 @@
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use dioxus_core::VirtualDom;
|
||||
use dioxus_ssr::Renderer;
|
||||
use dioxus::prelude::VirtualDom;
|
||||
use dioxus_ssr::{
|
||||
incremental::{IncrementalRendererConfig, RenderFreshness, WrapBody},
|
||||
Renderer,
|
||||
};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::prelude::ServeConfig;
|
||||
use crate::{prelude::*, server_context::with_server_context};
|
||||
use dioxus::prelude::*;
|
||||
|
||||
enum SsrRendererPool {
|
||||
Renderer(object_pool::Pool<Renderer>),
|
||||
Incremental(object_pool::Pool<dioxus_ssr::incremental::IncrementalRenderer>),
|
||||
}
|
||||
|
||||
impl SsrRendererPool {
|
||||
async fn render_to<P: Clone + Serialize + Send + Sync + 'static>(
|
||||
&self,
|
||||
cfg: &ServeConfig<P>,
|
||||
route: String,
|
||||
component: Component<P>,
|
||||
props: P,
|
||||
to: &mut WriteBuffer,
|
||||
server_context: &DioxusServerContext,
|
||||
) -> Result<RenderFreshness, dioxus_ssr::incremental::IncrementalRendererError> {
|
||||
let wrapper = FullstackRenderer { cfg };
|
||||
match self {
|
||||
Self::Renderer(pool) => {
|
||||
let server_context = Box::new(server_context.clone());
|
||||
let mut vdom = VirtualDom::new_with_props(component, props);
|
||||
|
||||
with_server_context(server_context, || {
|
||||
let _ = vdom.rebuild();
|
||||
});
|
||||
|
||||
let mut renderer = pool.pull(pre_renderer);
|
||||
|
||||
// SAFETY: The fullstack renderer will only write UTF-8 to the buffer.
|
||||
wrapper.render_before_body(&mut **to)?;
|
||||
renderer.render_to(to, &vdom)?;
|
||||
wrapper.render_after_body(&mut **to)?;
|
||||
|
||||
Ok(RenderFreshness::now(None))
|
||||
}
|
||||
Self::Incremental(pool) => {
|
||||
let mut renderer =
|
||||
pool.pull(|| incremental_pre_renderer(cfg.incremental.as_ref().unwrap()));
|
||||
Ok(renderer
|
||||
.render(
|
||||
route,
|
||||
component,
|
||||
props,
|
||||
&mut **to,
|
||||
|vdom| {
|
||||
let server_context = Box::new(server_context.clone());
|
||||
with_server_context(server_context, || {
|
||||
let _ = vdom.rebuild();
|
||||
});
|
||||
},
|
||||
&wrapper,
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// State used in server side rendering. This utilizes a pool of [`dioxus_ssr::Renderer`]s to cache static templates between renders.
|
||||
#[derive(Clone)]
|
||||
pub struct SSRState {
|
||||
// We keep a pool of renderers to avoid re-creating them on every request. They are boxed to make them very cheap to move
|
||||
renderers: Arc<object_pool::Pool<Renderer>>,
|
||||
renderers: Arc<SsrRendererPool>,
|
||||
}
|
||||
|
||||
impl Default for SSRState {
|
||||
fn default() -> Self {
|
||||
impl SSRState {
|
||||
pub(crate) fn new<P: Clone>(cfg: &ServeConfig<P>) -> Self {
|
||||
if cfg.incremental.is_some() {
|
||||
return Self {
|
||||
renderers: Arc::new(SsrRendererPool::Incremental(object_pool::Pool::new(
|
||||
10,
|
||||
|| incremental_pre_renderer(cfg.incremental.as_ref().unwrap()),
|
||||
))),
|
||||
};
|
||||
}
|
||||
|
||||
Self {
|
||||
renderers: Arc::new(object_pool::Pool::new(10, pre_renderer)),
|
||||
renderers: Arc::new(SsrRendererPool::Renderer(object_pool::Pool::new(
|
||||
10,
|
||||
pre_renderer,
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Render the application to HTML.
|
||||
pub fn render<'a, P: 'static + Clone + serde::Serialize + Send + Sync>(
|
||||
&'a self,
|
||||
route: String,
|
||||
cfg: &'a ServeConfig<P>,
|
||||
server_context: &'a DioxusServerContext,
|
||||
) -> impl std::future::Future<
|
||||
Output = Result<RenderResponse, dioxus_ssr::incremental::IncrementalRendererError>,
|
||||
> + Send
|
||||
+ 'a {
|
||||
async move {
|
||||
let mut html = WriteBuffer { buffer: Vec::new() };
|
||||
let ServeConfig { app, props, .. } = cfg;
|
||||
|
||||
let freshness = self
|
||||
.renderers
|
||||
.render_to(cfg, route, *app, props.clone(), &mut html, server_context)
|
||||
.await?;
|
||||
|
||||
Ok(RenderResponse {
|
||||
html: String::from_utf8(html.buffer).map_err(|err| {
|
||||
dioxus_ssr::incremental::IncrementalRendererError::Other(Box::new(err))
|
||||
})?,
|
||||
freshness,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SSRState {
|
||||
/// Render the application to HTML.
|
||||
pub fn render<P: 'static + Clone + serde::Serialize>(&self, cfg: &ServeConfig<P>) -> String {
|
||||
let ServeConfig { app, props, .. } = cfg;
|
||||
struct FullstackRenderer<'a, P: Clone + Send + Sync + 'static> {
|
||||
cfg: &'a ServeConfig<P>,
|
||||
}
|
||||
|
||||
let mut vdom = VirtualDom::new_with_props(*app, props.clone());
|
||||
impl<'a, P: Clone + Serialize + Send + Sync + 'static> dioxus_ssr::incremental::WrapBody
|
||||
for FullstackRenderer<'a, P>
|
||||
{
|
||||
fn render_before_body<R: std::io::Write>(
|
||||
&self,
|
||||
to: &mut R,
|
||||
) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> {
|
||||
let ServeConfig { index, .. } = &self.cfg;
|
||||
|
||||
let _ = vdom.rebuild();
|
||||
to.write_all(index.pre_main.as_bytes())?;
|
||||
|
||||
self.render_vdom(&vdom, cfg)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Render a VirtualDom to HTML.
|
||||
pub fn render_vdom<P: 'static + Clone + serde::Serialize>(
|
||||
fn render_after_body<R: std::io::Write>(
|
||||
&self,
|
||||
vdom: &VirtualDom,
|
||||
cfg: &ServeConfig<P>,
|
||||
) -> String {
|
||||
let ServeConfig { index, .. } = cfg;
|
||||
|
||||
let mut renderer = self.renderers.pull(pre_renderer);
|
||||
|
||||
let mut html = String::new();
|
||||
|
||||
html += &index.pre_main;
|
||||
|
||||
let _ = renderer.render_to(&mut html, vdom);
|
||||
|
||||
to: &mut R,
|
||||
) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> {
|
||||
// serialize the props
|
||||
let _ = crate::props_html::serialize_props::encode_in_element(&cfg.props, &mut html);
|
||||
crate::props_html::serialize_props::encode_in_element(&self.cfg.props, to)?;
|
||||
|
||||
#[cfg(all(debug_assertions, feature = "hot-reload"))]
|
||||
{
|
||||
@@ -82,19 +178,91 @@ impl SSRState {
|
||||
ws.onclose = reload_upon_connect;
|
||||
})()"#;
|
||||
|
||||
html += r#"<script>"#;
|
||||
html += disconnect_js;
|
||||
html += r#"</script>"#;
|
||||
to.write_all(r#"<script>"#.as_bytes())?;
|
||||
to.write_all(disconnect_js.as_bytes())?;
|
||||
to.write_all(r#"</script>"#.as_bytes())?;
|
||||
}
|
||||
|
||||
html += &index.post_main;
|
||||
let ServeConfig { index, .. } = &self.cfg;
|
||||
|
||||
html
|
||||
to.write_all(index.post_main.as_bytes())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A rendered response from the server.
|
||||
pub struct RenderResponse {
|
||||
pub(crate) html: String,
|
||||
pub(crate) freshness: RenderFreshness,
|
||||
}
|
||||
|
||||
impl RenderResponse {
|
||||
/// Get the rendered HTML.
|
||||
pub fn html(&self) -> &str {
|
||||
&self.html
|
||||
}
|
||||
|
||||
/// Get the freshness of the rendered HTML.
|
||||
pub fn freshness(&self) -> RenderFreshness {
|
||||
self.freshness
|
||||
}
|
||||
}
|
||||
|
||||
fn pre_renderer() -> Renderer {
|
||||
let mut renderer = Renderer::default();
|
||||
renderer.pre_render = true;
|
||||
renderer.into()
|
||||
}
|
||||
|
||||
fn incremental_pre_renderer(
|
||||
cfg: &IncrementalRendererConfig,
|
||||
) -> dioxus_ssr::incremental::IncrementalRenderer {
|
||||
let mut renderer = cfg.clone().build();
|
||||
renderer.renderer_mut().pre_render = true;
|
||||
renderer
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "ssr", feature = "router"))]
|
||||
/// Pre-caches all static routes
|
||||
pub async fn pre_cache_static_routes_with_props<Rt>(
|
||||
cfg: &crate::prelude::ServeConfig<crate::router::FullstackRouterConfig<Rt>>,
|
||||
) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError>
|
||||
where
|
||||
Rt: dioxus_router::prelude::Routable + Send + Sync + Serialize,
|
||||
<Rt as std::str::FromStr>::Err: std::fmt::Display,
|
||||
{
|
||||
let wrapper = FullstackRenderer { cfg };
|
||||
let mut renderer = incremental_pre_renderer(
|
||||
cfg.incremental
|
||||
.as_ref()
|
||||
.expect("incremental renderer config must be set to pre-cache static routes"),
|
||||
);
|
||||
|
||||
dioxus_router::incremental::pre_cache_static_routes::<Rt, _>(&mut renderer, &wrapper).await
|
||||
}
|
||||
|
||||
struct WriteBuffer {
|
||||
buffer: Vec<u8>,
|
||||
}
|
||||
|
||||
impl std::fmt::Write for WriteBuffer {
|
||||
fn write_str(&mut self, s: &str) -> std::fmt::Result {
|
||||
self.buffer.extend_from_slice(s.as_bytes());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for WriteBuffer {
|
||||
type Target = Vec<u8>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.buffer
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for WriteBuffer {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.buffer
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
//! Fullstack router intigration
|
||||
#![allow(non_snake_case)]
|
||||
use dioxus::prelude::*;
|
||||
|
||||
/// Used by the launch macro
|
||||
#[doc(hidden)]
|
||||
pub fn RouteWithCfg<R>(cx: Scope<FullstackRouterConfig<R>>) -> Element
|
||||
where
|
||||
R: dioxus_router::prelude::Routable,
|
||||
<R as std::str::FromStr>::Err: std::fmt::Display,
|
||||
{
|
||||
use dioxus_router::prelude::RouterConfig;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
let context = crate::prelude::server_context();
|
||||
|
||||
let cfg = *cx.props;
|
||||
render! {
|
||||
dioxus_router::prelude::GenericRouter::<R> {
|
||||
config: move || {
|
||||
RouterConfig::default()
|
||||
.failure_external_navigation(cfg.failure_external_navigation)
|
||||
.history({
|
||||
#[cfg(feature = "ssr")]
|
||||
let history = dioxus_router::prelude::MemoryHistory::with_initial_path(
|
||||
context
|
||||
.request_parts().unwrap()
|
||||
.uri
|
||||
.to_string()
|
||||
.parse()
|
||||
.unwrap_or_else(|err| {
|
||||
log::error!("Failed to parse uri: {}", err);
|
||||
"/"
|
||||
.parse()
|
||||
.unwrap_or_else(|err| {
|
||||
panic!("Failed to parse uri: {}", err);
|
||||
})
|
||||
}),
|
||||
);
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
let history = dioxus_router::prelude::WebHistory::new(
|
||||
None,
|
||||
cfg.scroll_restoration,
|
||||
);
|
||||
history
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_external_navigation_handler<R>() -> fn(Scope) -> Element
|
||||
where
|
||||
R: dioxus_router::prelude::Routable,
|
||||
<R as std::str::FromStr>::Err: std::fmt::Display,
|
||||
{
|
||||
dioxus_router::prelude::FailureExternalNavigation::<R>
|
||||
}
|
||||
|
||||
/// The configeration for the router
|
||||
#[derive(Props, serde::Serialize, serde::Deserialize)]
|
||||
pub struct FullstackRouterConfig<R>
|
||||
where
|
||||
R: dioxus_router::prelude::Routable,
|
||||
<R as std::str::FromStr>::Err: std::fmt::Display,
|
||||
{
|
||||
#[serde(skip)]
|
||||
#[serde(default = "default_external_navigation_handler::<R>")]
|
||||
failure_external_navigation: fn(Scope) -> Element,
|
||||
scroll_restoration: bool,
|
||||
#[serde(skip)]
|
||||
phantom: std::marker::PhantomData<R>,
|
||||
}
|
||||
|
||||
impl<R> Clone for FullstackRouterConfig<R>
|
||||
where
|
||||
R: dioxus_router::prelude::Routable,
|
||||
<R as std::str::FromStr>::Err: std::fmt::Display,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
failure_external_navigation: self.failure_external_navigation,
|
||||
scroll_restoration: self.scroll_restoration,
|
||||
phantom: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> Copy for FullstackRouterConfig<R>
|
||||
where
|
||||
R: dioxus_router::prelude::Routable,
|
||||
<R as std::str::FromStr>::Err: std::fmt::Display,
|
||||
{
|
||||
}
|
||||
|
||||
impl<R> Default for FullstackRouterConfig<R>
|
||||
where
|
||||
R: dioxus_router::prelude::Routable,
|
||||
<R as std::str::FromStr>::Err: std::fmt::Display,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
failure_external_navigation: dioxus_router::prelude::FailureExternalNavigation::<R>,
|
||||
scroll_restoration: true,
|
||||
phantom: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
#![allow(non_snake_case)]
|
||||
//! Configeration 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_core::Component;
|
||||
use dioxus::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)]
|
||||
@@ -14,6 +17,40 @@ pub struct ServeConfigBuilder<P: Clone> {
|
||||
pub(crate) root_id: Option<&'static str>,
|
||||
pub(crate) index_path: Option<&'static str>,
|
||||
pub(crate) assets_path: Option<&'static str>,
|
||||
pub(crate) incremental:
|
||||
Option<std::sync::Arc<dioxus_ssr::incremental::IncrementalRendererConfig>>,
|
||||
}
|
||||
|
||||
/// A template for incremental rendering that does nothing.
|
||||
#[derive(Default, Clone)]
|
||||
pub struct EmptyIncrementalRenderTemplate;
|
||||
|
||||
impl dioxus_ssr::incremental::WrapBody for EmptyIncrementalRenderTemplate {
|
||||
fn render_after_body<R: std::io::Write>(
|
||||
&self,
|
||||
_: &mut R,
|
||||
) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_before_body<R: std::io::Write>(
|
||||
&self,
|
||||
_: &mut R,
|
||||
) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "router")]
|
||||
impl<R> ServeConfigBuilder<FullstackRouterConfig<R>>
|
||||
where
|
||||
R: dioxus_router::prelude::Routable,
|
||||
<R as std::str::FromStr>::Err: std::fmt::Display,
|
||||
{
|
||||
/// Create a new ServeConfigBuilder to serve a router on the server.
|
||||
pub fn new_with_router(cfg: FullstackRouterConfig<R>) -> Self {
|
||||
Self::new(RouteWithCfg::<R>, cfg)
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: Clone> ServeConfigBuilder<P> {
|
||||
@@ -25,9 +62,16 @@ impl<P: Clone> ServeConfigBuilder<P> {
|
||||
root_id: None,
|
||||
index_path: None,
|
||||
assets_path: None,
|
||||
incremental: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Enable incremental static generation
|
||||
pub fn incremental(mut self, cfg: dioxus_ssr::incremental::IncrementalRendererConfig) -> Self {
|
||||
self.incremental = Some(std::sync::Arc::new(cfg));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the path of the index.html file to be served. (defaults to {assets_path}/index.html)
|
||||
pub fn index_path(mut self, index_path: &'static str) -> Self {
|
||||
self.index_path = Some(index_path);
|
||||
@@ -64,6 +108,7 @@ impl<P: Clone> ServeConfigBuilder<P> {
|
||||
props: self.props,
|
||||
index,
|
||||
assets_path,
|
||||
incremental: self.incremental,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,6 +151,8 @@ pub struct ServeConfig<P: Clone> {
|
||||
pub(crate) props: P,
|
||||
pub(crate) index: IndexHtml,
|
||||
pub(crate) assets_path: &'static str,
|
||||
pub(crate) incremental:
|
||||
Option<std::sync::Arc<dioxus_ssr::incremental::IncrementalRendererConfig>>,
|
||||
}
|
||||
|
||||
impl<P: Clone> From<ServeConfigBuilder<P>> for ServeConfig<P> {
|
||||
|
||||
@@ -1,63 +1,33 @@
|
||||
use dioxus_core::ScopeState;
|
||||
pub use server_fn_impl::*;
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
|
||||
/// A trait for an object that contains a server context
|
||||
pub trait HasServerContext {
|
||||
/// Get the server context from the state
|
||||
fn server_context(&self) -> DioxusServerContext;
|
||||
|
||||
/// A shortcut for `self.server_context()`
|
||||
fn sc(&self) -> DioxusServerContext {
|
||||
self.server_context()
|
||||
}
|
||||
}
|
||||
|
||||
impl HasServerContext for &ScopeState {
|
||||
fn server_context(&self) -> DioxusServerContext {
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
self.consume_context().expect("No server context found")
|
||||
}
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
{
|
||||
DioxusServerContext {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A shared context for server functions that contains infomation about the request and middleware state.
|
||||
/// A shared context for server functions that contains information about the request and middleware state.
|
||||
/// This allows you to pass data between your server framework and the server functions. This can be used to pass request information or information about the state of the server. For example, you could pass authentication data though this context to your server functions.
|
||||
///
|
||||
/// You should not construct this directly inside components. Instead use the `HasServerContext` trait to get the server context from the scope.
|
||||
#[derive(Clone)]
|
||||
pub struct DioxusServerContext {
|
||||
#[cfg(feature = "ssr")]
|
||||
shared_context: std::sync::Arc<
|
||||
std::sync::RwLock<anymap::Map<dyn anymap::any::Any + Send + Sync + 'static>>,
|
||||
>,
|
||||
#[cfg(feature = "ssr")]
|
||||
headers: std::sync::Arc<std::sync::RwLock<hyper::header::HeaderMap>>,
|
||||
#[cfg(feature = "ssr")]
|
||||
pub(crate) parts: std::sync::Arc<RequestParts>,
|
||||
response_parts: std::sync::Arc<std::sync::RwLock<http::response::Parts>>,
|
||||
pub(crate) parts: Arc<RwLock<http::request::Parts>>,
|
||||
}
|
||||
|
||||
#[allow(clippy::derivable_impls)]
|
||||
impl Default for DioxusServerContext {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
#[cfg(feature = "ssr")]
|
||||
shared_context: std::sync::Arc::new(std::sync::RwLock::new(anymap::Map::new())),
|
||||
#[cfg(feature = "ssr")]
|
||||
headers: Default::default(),
|
||||
#[cfg(feature = "ssr")]
|
||||
parts: Default::default(),
|
||||
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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub use server_fn_impl::*;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
mod server_fn_impl {
|
||||
use super::*;
|
||||
use std::sync::LockResult;
|
||||
@@ -68,11 +38,13 @@ mod server_fn_impl {
|
||||
|
||||
impl DioxusServerContext {
|
||||
/// Create a new server context from a request
|
||||
pub fn new(parts: impl Into<Arc<RequestParts>>) -> Self {
|
||||
pub fn new(parts: impl Into<Arc<RwLock<http::request::Parts>>>) -> Self {
|
||||
Self {
|
||||
parts: parts.into(),
|
||||
shared_context: Arc::new(RwLock::new(SendSyncAnyMap::new())),
|
||||
headers: Default::default(),
|
||||
response_parts: std::sync::Arc::new(RwLock::new(
|
||||
http::response::Response::new(()).into_parts().0,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,69 +64,164 @@ mod server_fn_impl {
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
/// Get the headers from the server context
|
||||
pub fn response_headers(&self) -> RwLockReadGuard<'_, hyper::header::HeaderMap> {
|
||||
self.try_response_headers()
|
||||
.expect("Failed to get headers from server context")
|
||||
/// Get the response parts from the server context
|
||||
pub fn response_parts(&self) -> LockResult<RwLockReadGuard<'_, http::response::Parts>> {
|
||||
self.response_parts.read()
|
||||
}
|
||||
|
||||
/// Try to get the headers from the server context
|
||||
pub fn try_response_headers(
|
||||
/// Get the response parts from the server context
|
||||
pub fn response_parts_mut(
|
||||
&self,
|
||||
) -> LockResult<RwLockReadGuard<'_, hyper::header::HeaderMap>> {
|
||||
self.headers.read()
|
||||
}
|
||||
|
||||
/// Get the headers mutably from the server context
|
||||
pub fn response_headers_mut(&self) -> RwLockWriteGuard<'_, hyper::header::HeaderMap> {
|
||||
self.try_response_headers_mut()
|
||||
.expect("Failed to get headers mutably from server context")
|
||||
}
|
||||
|
||||
/// Try to get the headers mut from the server context
|
||||
pub fn try_response_headers_mut(
|
||||
&self,
|
||||
) -> LockResult<RwLockWriteGuard<'_, hyper::header::HeaderMap>> {
|
||||
self.headers.write()
|
||||
}
|
||||
|
||||
pub(crate) fn take_response_headers(&self) -> hyper::header::HeaderMap {
|
||||
let mut headers = self.headers.write().unwrap();
|
||||
std::mem::take(&mut *headers)
|
||||
) -> LockResult<RwLockWriteGuard<'_, http::response::Parts>> {
|
||||
self.response_parts.write()
|
||||
}
|
||||
|
||||
/// 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) -> &RequestParts {
|
||||
&self.parts
|
||||
pub fn request_parts(
|
||||
&self,
|
||||
) -> std::sync::LockResult<RwLockReadGuard<'_, http::request::Parts>> {
|
||||
self.parts.read()
|
||||
}
|
||||
}
|
||||
|
||||
/// Associated parts of an HTTP Request
|
||||
#[derive(Debug, Default)]
|
||||
pub struct RequestParts {
|
||||
/// The request's method
|
||||
pub method: http::Method,
|
||||
/// The request's URI
|
||||
pub uri: http::Uri,
|
||||
/// The request's version
|
||||
pub version: http::Version,
|
||||
/// The request's headers
|
||||
pub headers: http::HeaderMap<http::HeaderValue>,
|
||||
/// The request's extensions
|
||||
pub extensions: http::Extensions,
|
||||
}
|
||||
/// 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()
|
||||
}
|
||||
|
||||
impl From<http::request::Parts> for RequestParts {
|
||||
fn from(parts: http::request::Parts) -> Self {
|
||||
Self {
|
||||
method: parts.method,
|
||||
uri: parts.uri,
|
||||
version: parts.version,
|
||||
headers: parts.headers,
|
||||
extensions: parts.extensions,
|
||||
}
|
||||
/// Extract some part from the request
|
||||
pub async fn extract<R: std::error::Error, T: FromServerContext<Rejection = R>>(
|
||||
&self,
|
||||
) -> Result<T, R> {
|
||||
T::from_request(self).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::thread_local! {
|
||||
static SERVER_CONTEXT: std::cell::RefCell<Box<DioxusServerContext>> = std::cell::RefCell::new(Box::new(DioxusServerContext::default() ));
|
||||
}
|
||||
|
||||
/// Get information about the current server request.
|
||||
///
|
||||
/// This function will only provide the current server context if it is called from a server function or on the server rendering a request.
|
||||
pub fn server_context() -> DioxusServerContext {
|
||||
SERVER_CONTEXT.with(|ctx| *ctx.borrow().clone())
|
||||
}
|
||||
|
||||
/// Extract some part from the current server request.
|
||||
///
|
||||
/// This function will only provide the current server context if it is called from a server function or on the server rendering a request.
|
||||
pub async fn extract<E: FromServerContext<I>, I>() -> Result<E, E::Rejection> {
|
||||
E::from_request(&server_context()).await
|
||||
}
|
||||
|
||||
pub(crate) fn with_server_context<O>(
|
||||
context: Box<DioxusServerContext>,
|
||||
f: impl FnOnce() -> O,
|
||||
) -> (O, Box<DioxusServerContext>) {
|
||||
// before polling the future, we need to set the context
|
||||
let prev_context = SERVER_CONTEXT.with(|ctx| ctx.replace(context));
|
||||
// poll the future, which may call server_context()
|
||||
let result = f();
|
||||
// after polling the future, we need to restore the context
|
||||
(result, SERVER_CONTEXT.with(|ctx| ctx.replace(prev_context)))
|
||||
}
|
||||
|
||||
/// A future that provides the server context to the inner future
|
||||
#[pin_project::pin_project]
|
||||
pub struct ProvideServerContext<F: std::future::Future> {
|
||||
context: Option<Box<DioxusServerContext>>,
|
||||
#[pin]
|
||||
f: F,
|
||||
}
|
||||
|
||||
impl<F: std::future::Future> ProvideServerContext<F> {
|
||||
/// Create a new future that provides the server context to the inner future
|
||||
pub fn new(f: F, context: DioxusServerContext) -> Self {
|
||||
Self {
|
||||
context: Some(Box::new(context)),
|
||||
f,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: std::future::Future> std::future::Future for ProvideServerContext<F> {
|
||||
type Output = F::Output;
|
||||
|
||||
fn poll(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Self::Output> {
|
||||
let this = self.project();
|
||||
let context = this.context.take().unwrap();
|
||||
let (result, context) = with_server_context(context, || this.f.poll(cx));
|
||||
*this.context = Some(context);
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for extracting types from the server context
|
||||
#[async_trait::async_trait(?Send)]
|
||||
pub trait FromServerContext<I = ()>: Sized {
|
||||
/// The error type returned when extraction fails. This type must implement `std::error::Error`.
|
||||
type Rejection: std::error::Error;
|
||||
|
||||
/// Extract this type from the server context.
|
||||
async fn from_request(req: &DioxusServerContext) -> Result<Self, Self::Rejection>;
|
||||
}
|
||||
|
||||
/// A type was not found in the server context
|
||||
pub struct NotFoundInServerContext<T: 'static>(std::marker::PhantomData<T>);
|
||||
|
||||
impl<T: 'static> std::fmt::Debug for NotFoundInServerContext<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let type_name = std::any::type_name::<T>();
|
||||
write!(f, "`{type_name}` not found in server context")
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static> std::fmt::Display for NotFoundInServerContext<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let type_name = std::any::type_name::<T>();
|
||||
write!(f, "`{type_name}` not found in server context")
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static> std::error::Error for NotFoundInServerContext<T> {}
|
||||
|
||||
pub struct FromContext<T: std::marker::Send + std::marker::Sync + Clone + 'static>(pub(crate) T);
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
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(|| {
|
||||
NotFoundInServerContext::<T>(std::marker::PhantomData::<T>)
|
||||
})?))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "axum")]
|
||||
/// An adapter for axum extractors for the server context
|
||||
pub struct Axum;
|
||||
|
||||
#[cfg(feature = "axum")]
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl<
|
||||
I: axum::extract::FromRequestParts<(), Rejection = R>,
|
||||
R: axum::response::IntoResponse + std::error::Error,
|
||||
> FromServerContext<Axum> for I
|
||||
{
|
||||
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?)
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
use crate::server_context::DioxusServerContext;
|
||||
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
#[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<DioxusServerContext>);
|
||||
pub struct ServerFnTraitObj(server_fn::ServerFnTraitObj<()>);
|
||||
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
impl std::ops::Deref for ServerFnTraitObj {
|
||||
type Target = server_fn::ServerFnTraitObj<DioxusServerContext>;
|
||||
type Target = server_fn::ServerFnTraitObj<()>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
@@ -33,9 +31,7 @@ impl ServerFnTraitObj {
|
||||
}
|
||||
|
||||
/// Create a new `ServerFnTraitObj` from a `server_fn::ServerFnTraitObj`.
|
||||
pub const fn from_generic_server_fn(
|
||||
server_fn: server_fn::ServerFnTraitObj<DioxusServerContext>,
|
||||
) -> Self {
|
||||
pub const fn from_generic_server_fn(server_fn: server_fn::ServerFnTraitObj<()>) -> Self {
|
||||
Self(server_fn)
|
||||
}
|
||||
}
|
||||
@@ -43,9 +39,42 @@ impl ServerFnTraitObj {
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
server_fn::inventory::collect!(ServerFnTraitObj);
|
||||
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
/// Middleware for a server function
|
||||
pub struct ServerFnMiddleware {
|
||||
/// The prefix of the server function.
|
||||
pub prefix: &'static str,
|
||||
/// The url of the server function.
|
||||
pub url: &'static str,
|
||||
/// The middleware layers.
|
||||
pub middleware: fn() -> Vec<std::sync::Arc<dyn crate::layer::Layer>>,
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
pub(crate) static MIDDLEWARE: once_cell::sync::Lazy<
|
||||
std::collections::HashMap<
|
||||
(&'static str, &'static str),
|
||||
Vec<std::sync::Arc<dyn crate::layer::Layer>>,
|
||||
>,
|
||||
> = once_cell::sync::Lazy::new(|| {
|
||||
let mut map: std::collections::HashMap<
|
||||
(&'static str, &'static str),
|
||||
Vec<std::sync::Arc<dyn crate::layer::Layer>>,
|
||||
> = std::collections::HashMap::new();
|
||||
for middleware in server_fn::inventory::iter::<ServerFnMiddleware> {
|
||||
map.entry((middleware.prefix, middleware.url))
|
||||
.or_default()
|
||||
.extend((middleware.middleware)().iter().cloned());
|
||||
}
|
||||
map
|
||||
});
|
||||
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
server_fn::inventory::collect!(ServerFnMiddleware);
|
||||
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
/// A server function that can be called on serializable arguments and returns a serializable result.
|
||||
pub type ServerFunction = server_fn::SerializedFnTraitObj<DioxusServerContext>;
|
||||
pub type ServerFunction = server_fn::SerializedFnTraitObj<()>;
|
||||
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
#[allow(clippy::type_complexity)]
|
||||
@@ -64,7 +93,7 @@ static REGISTERED_SERVER_FUNCTIONS: once_cell::sync::Lazy<
|
||||
pub struct DioxusServerFnRegistry;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
impl server_fn::ServerFunctionRegistry<DioxusServerContext> for DioxusServerFnRegistry {
|
||||
impl server_fn::ServerFunctionRegistry<()> for DioxusServerFnRegistry {
|
||||
type Error = ServerRegistrationFnError;
|
||||
|
||||
fn register_explicit(
|
||||
@@ -105,7 +134,7 @@ impl server_fn::ServerFunctionRegistry<DioxusServerContext> for DioxusServerFnRe
|
||||
}
|
||||
|
||||
/// Returns the server function registered at the given URL, or `None` if no function is registered at that URL.
|
||||
fn get(url: &str) -> Option<server_fn::ServerFnTraitObj<DioxusServerContext>> {
|
||||
fn get(url: &str) -> Option<server_fn::ServerFnTraitObj<()>> {
|
||||
REGISTERED_SERVER_FUNCTIONS
|
||||
.read()
|
||||
.ok()
|
||||
@@ -113,7 +142,7 @@ impl server_fn::ServerFunctionRegistry<DioxusServerContext> for DioxusServerFnRe
|
||||
}
|
||||
|
||||
/// Returns the server function registered at the given URL, or `None` if no function is registered at that URL.
|
||||
fn get_trait_obj(url: &str) -> Option<server_fn::ServerFnTraitObj<DioxusServerContext>> {
|
||||
fn get_trait_obj(url: &str) -> Option<server_fn::ServerFnTraitObj<()>> {
|
||||
Self::get(url)
|
||||
}
|
||||
|
||||
@@ -155,7 +184,7 @@ pub enum ServerRegistrationFnError {
|
||||
/// 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<DioxusServerContext> {
|
||||
pub trait DioxusServerFn: server_fn::ServerFn<()> {
|
||||
/// Registers the server function, allowing the client to query it by URL.
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
fn register_explicit() -> Result<(), server_fn::ServerFnError> {
|
||||
@@ -163,4 +192,4 @@ pub trait DioxusServerFn: server_fn::ServerFn<DioxusServerContext> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DioxusServerFn for T where T: server_fn::ServerFn<DioxusServerContext> {}
|
||||
impl<T> DioxusServerFn for T where T: server_fn::ServerFn<()> {}
|
||||
Reference in new issue
Block a user