From fdc8ebd1b196fddf5abd2cbc50cbd98c9fc0f912 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 30 Mar 2023 10:34:13 -0500 Subject: [PATCH] create fullstack hello world example --- packages/server/.gitignore | 1 + packages/server/Cargo.toml | 23 ++- .../server/examples/hello-world/.gitignore | 2 + .../server/examples/hello-world/Cargo.toml | 20 +++ .../server/examples/hello-world/src/main.rs | 70 +++++++++ packages/server/server_macro/Cargo.toml | 6 + packages/server/server_macro/src/lib.rs | 10 +- packages/server/src/adapters/axum_adapter.rs | 49 +++++- packages/server/src/adapters/mod.rs | 2 +- packages/server/src/lib.rs | 148 ++++++------------ packages/server/src/server_fn.rs | 98 ++++++++++++ 11 files changed, 312 insertions(+), 117 deletions(-) create mode 100644 packages/server/.gitignore create mode 100644 packages/server/examples/hello-world/.gitignore create mode 100644 packages/server/examples/hello-world/Cargo.toml create mode 100644 packages/server/examples/hello-world/src/main.rs create mode 100644 packages/server/src/server_fn.rs diff --git a/packages/server/.gitignore b/packages/server/.gitignore new file mode 100644 index 000000000..1de565933 --- /dev/null +++ b/packages/server/.gitignore @@ -0,0 +1 @@ +target \ No newline at end of file diff --git a/packages/server/Cargo.toml b/packages/server/Cargo.toml index 4e3028aaa..af7e03859 100644 --- a/packages/server/Cargo.toml +++ b/packages/server/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "server" +name = "dioxus-server" version = "0.1.0" edition = "2021" @@ -7,28 +7,37 @@ edition = "2021" [dependencies] server_fn = { version = "0.2.4", features = ["stable"] } +server_macro = { path = "server_macro" } # warp warp = { version = "0.3.3", optional = true } # axum axum = { version = "0.6.1", optional = true, features = ["ws"] } +tower-http = { version = "0.4.0", optional = true, features = ["fs"] } +hyper = { version = "0.14.25", optional = true } # salvo salvo = { version = "0.37.7", optional = true, features = ["ws"] } serde = "1.0.159" -dioxus = { path = "../dioxus", version = "^0.3.0" } +dioxus-core = { path = "../core", version = "^0.3.0" } +dioxus-ssr = { path = "../ssr", version = "^0.3.0", optional = true } log = "0.4.17" once_cell = "1.17.1" thiserror = "1.0.40" -hyper = "0.14.25" -tokio = { version = "1.27.0", features = ["full"] } +tokio = { version = "1.27.0", features = ["full"], optional = true } [features] -default = ["axum", "ssr"] +default = [] warp = ["dep:warp"] -axum = ["dep:axum"] +axum = ["dep:axum", "tower-http", "hyper"] salvo = ["dep:salvo"] -ssr = ["server_fn/ssr"] +ssr = ["server_fn/ssr", "tokio", "dioxus-ssr"] + +[workspace] +members = [ + "server_macro", + "examples/hello-world", +] \ No newline at end of file diff --git a/packages/server/examples/hello-world/.gitignore b/packages/server/examples/hello-world/.gitignore new file mode 100644 index 000000000..6047329c6 --- /dev/null +++ b/packages/server/examples/hello-world/.gitignore @@ -0,0 +1,2 @@ +dist +target \ No newline at end of file diff --git a/packages/server/examples/hello-world/Cargo.toml b/packages/server/examples/hello-world/Cargo.toml new file mode 100644 index 000000000..846c03ea4 --- /dev/null +++ b/packages/server/examples/hello-world/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "hello-world" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +dioxus-web = { path = "../../../web", features=["hydrate"], optional = true } +dioxus = { path = "../../../dioxus" } +dioxus-server = { path = "../../" } +axum = { version = "0.6.12", optional = true } +tokio = { version = "1.27.0", features = ["full"], optional = true } +serde = "1.0.159" +tracing-subscriber = "0.3.16" +tracing = "0.1.37" + +[features] +ssr = ["axum", "tokio", "dioxus-server/ssr", "dioxus-server/axum"] +web = ["dioxus-web"] diff --git a/packages/server/examples/hello-world/src/main.rs b/packages/server/examples/hello-world/src/main.rs new file mode 100644 index 000000000..9b9084963 --- /dev/null +++ b/packages/server/examples/hello-world/src/main.rs @@ -0,0 +1,70 @@ +#![allow(non_snake_case)] +use dioxus::prelude::*; +use dioxus_server::prelude::*; + +fn main() { + #[cfg(feature = "web")] + dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true)); + #[cfg(feature = "ssr")] + { + PostServerData::register().unwrap(); + GetServerData::register().unwrap(); + 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( + "Hello, world!", + "hello-world", + None, + None, + "", + app, + ) + .into_make_service(), + ) + .await + .unwrap(); + }); + } +} + +fn app(cx: Scope) -> Element { + let mut count = use_state(cx, || 0); + let text = use_state(cx, || "...".to_string()); + + cx.render(rsx! { + h1 { "High-Five counter: {count}" } + button { onclick: move |_| count += 1, "Up high!" } + button { onclick: move |_| count -= 1, "Down low!" } + button { + onclick: move |_| { + to_owned![text]; + async move { + if let Ok(data) = get_server_data().await { + println!("Client received: {}", data); + text.set(data.clone()); + post_server_data(data).await.unwrap(); + } + } + }, + "Run a server function" + } + "Server said: {text}" + }) +} + +#[server(PostServerData)] +async fn post_server_data(data: String) -> Result<(), ServerFnError> { + println!("Server received: {}", data); + + Ok(()) +} + +#[server(GetServerData)] +async fn get_server_data() -> Result { + Ok("Hello from the server!".to_string()) +} diff --git a/packages/server/server_macro/Cargo.toml b/packages/server/server_macro/Cargo.toml index 6e8c69b46..cb285f517 100644 --- a/packages/server/server_macro/Cargo.toml +++ b/packages/server/server_macro/Cargo.toml @@ -6,3 +6,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +quote = "1.0.26" +server_fn_macro = { version = "0.2.4", features = ["stable"] } +syn = { version = "1", features = ["full"] } + +[lib] +proc-macro = true diff --git a/packages/server/server_macro/src/lib.rs b/packages/server/server_macro/src/lib.rs index 439a9e7b2..f2a2e1665 100644 --- a/packages/server/server_macro/src/lib.rs +++ b/packages/server/server_macro/src/lib.rs @@ -1,3 +1,7 @@ +use proc_macro::TokenStream; +use quote::ToTokens; +use server_fn_macro::*; + /// Declares that a function is a [server function](leptos_server). This means that /// its body will only run on the server, i.e., when the `ssr` feature is enabled. /// @@ -52,14 +56,14 @@ #[proc_macro_attribute] pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { let context = ServerContext { - ty: syn::parse_quote!(Scope), - path: syn::parse_quote!(::leptos::Scope), + ty: syn::parse_quote!(DioxusServerContext), + path: syn::parse_quote!(::dioxus_server::prelude::DioxusServerContext), }; match server_macro_impl( args.into(), s.into(), Some(context), - Some(syn::parse_quote!(::leptos::server_fn)), + Some(syn::parse_quote!(::dioxus_server::prelude::server_fn)), ) { Err(e) => e.to_compile_error().into(), Ok(s) => s.to_token_stream().into(), diff --git a/packages/server/src/adapters/axum_adapter.rs b/packages/server/src/adapters/axum_adapter.rs index 3fb0a1d4e..70513bdb3 100644 --- a/packages/server/src/adapters/axum_adapter.rs +++ b/packages/server/src/adapters/axum_adapter.rs @@ -4,33 +4,68 @@ use axum::{ body::{self, Body, BoxBody, Full}, http::{HeaderMap, Request, Response, StatusCode}, response::IntoResponse, - routing::post, + routing::{get, post}, Router, }; +use dioxus_core::Component; use server_fn::{Payload, ServerFunctionRegistry}; use tokio::task::spawn_blocking; -use crate::{DioxusServerContext, DioxusServerFnRegistry, ServerFnTraitObj}; +use crate::{ + dioxus_ssr_html, + server_fn::{DioxusServerContext, DioxusServerFnRegistry, ServerFnTraitObj}, +}; -trait DioxusRouterExt { - fn register_server_fns(self) -> Self; +pub trait DioxusRouterExt { + fn register_server_fns(self, server_fn_route: &'static str) -> Self; + fn serve_dioxus_application( + self, + title: &'static str, + application_name: &'static str, + base_path: Option<&'static str>, + head: Option<&'static str>, + server_fn_route: &'static str, + app: Component, + ) -> Self; } impl DioxusRouterExt for Router { - fn register_server_fns(self) -> Self { + fn register_server_fns(self, server_fn_route: &'static str) -> Self { let mut router = self; for server_fn_path in DioxusServerFnRegistry::paths_registered() { let func = DioxusServerFnRegistry::get(server_fn_path).unwrap(); + let full_route = format!("{server_fn_route}/{server_fn_path}"); router = router.route( - server_fn_path, + &full_route, post(move |headers: HeaderMap, body: Request| async move { server_fn_handler(DioxusServerContext {}, func.clone(), headers, body).await - // todo!() }), ); } router } + + fn serve_dioxus_application( + self, + title: &'static str, + application_name: &'static str, + base_path: Option<&'static str>, + head: Option<&'static str>, + server_fn_route: &'static str, + app: Component, + ) -> Self { + use tower_http::services::ServeDir; + + // Serve the dist folder and the index.html file + let serve_dir = ServeDir::new("dist"); + + self.register_server_fns(server_fn_route) + .nest_service("/", serve_dir) + .fallback_service(get(move || { + let rendered = dioxus_ssr_html(title, application_name, base_path, head, app); + async move { Full::from(rendered) } + })) + } } async fn server_fn_handler( diff --git a/packages/server/src/adapters/mod.rs b/packages/server/src/adapters/mod.rs index ce2670dde..c30365989 100644 --- a/packages/server/src/adapters/mod.rs +++ b/packages/server/src/adapters/mod.rs @@ -1,2 +1,2 @@ #[cfg(feature = "axum")] -mod axum_adapter; +pub mod axum_adapter; diff --git a/packages/server/src/lib.rs b/packages/server/src/lib.rs index c0ec07e06..c78f3471b 100644 --- a/packages/server/src/lib.rs +++ b/packages/server/src/lib.rs @@ -1,104 +1,54 @@ +#[allow(unused)] +use dioxus_core::prelude::*; + mod adapters; +mod server_fn; -// #[server(ReadPosts, "api")] -// async fn testing(rx: i32) -> Result { -// Ok(0) -// } - -pub struct DioxusServerContext {} - -#[cfg(any(feature = "ssr", doc))] -type ServerFnTraitObj = server_fn::ServerFnTraitObj; - -#[cfg(any(feature = "ssr", doc))] -static REGISTERED_SERVER_FUNCTIONS: once_cell::sync::Lazy< - std::sync::Arc< - std::sync::RwLock< - std::collections::HashMap<&'static str, std::sync::Arc>, - >, - >, -> = once_cell::sync::Lazy::new(Default::default); - -#[cfg(any(feature = "ssr", doc))] -/// The registry of all Dioxus server functions. -pub struct DioxusServerFnRegistry; - -#[cfg(any(feature = "ssr"))] -impl server_fn::ServerFunctionRegistry for DioxusServerFnRegistry { - type Error = ServerRegistrationFnError; - - fn register( - url: &'static str, - server_function: std::sync::Arc, - ) -> Result<(), Self::Error> { - // store it in the hashmap - let mut write = REGISTERED_SERVER_FUNCTIONS - .write() - .map_err(|e| ServerRegistrationFnError::Poisoned(e.to_string()))?; - let prev = write.insert(url, server_function); - - // if there was already a server function with this key, - // return Err - match prev { - Some(_) => Err(ServerRegistrationFnError::AlreadyRegistered(format!( - "There was already a server function registered at {:?}. \ - This can happen if you use the same server function name \ - in two different modules - on `stable` or in `release` mode.", - url - ))), - None => Ok(()), - } - } - - /// Returns the server function registered at the given URL, or `None` if no function is registered at that URL. - fn get(url: &str) -> Option> { - REGISTERED_SERVER_FUNCTIONS - .read() - .ok() - .and_then(|fns| fns.get(url).cloned()) - } - - /// Returns a list of all registered server functions. - fn paths_registered() -> Vec<&'static str> { - REGISTERED_SERVER_FUNCTIONS - .read() - .ok() - .map(|fns| fns.keys().cloned().collect()) - .unwrap_or_default() - } +pub mod prelude { + #[cfg(feature = "axum")] + pub use crate::adapters::axum_adapter::*; + pub use crate::server_fn::{DioxusServerContext, ServerFn}; + pub use server_fn::{self, ServerFn as _, ServerFnError}; + pub use server_macro::*; } -#[cfg(any(feature = "ssr", doc))] -/// Errors that can occur when registering a server function. -#[derive(thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize)] -pub enum ServerRegistrationFnError { - /// The server function is already registered. - #[error("The server function {0} is already registered")] - AlreadyRegistered(String), - /// The server function registry is poisoned. - #[error("The server function registry is poisoned: {0}")] - Poisoned(String), +#[cfg(feature = "ssr")] +fn dioxus_ssr_html( + title: &str, + application_name: &str, + base_path: Option<&str>, + head: Option<&str>, + app: Component, +) -> String { + let mut vdom = VirtualDom::new(app); + let _ = vdom.rebuild(); + let renderered = dioxus_ssr::pre_render(&vdom); + let base_path = base_path.unwrap_or("."); + let head = head.unwrap_or_default(); + format!( + r#" + + + + {title} + + + + {head} + + +
+ {renderered} +
+ + +"# + ) } - -/// Defines a "server function." A server function can be called from the server or the client, -/// but the body of its code will only be run on the server, i.e., if a crate feature `ssr` is enabled. -/// -/// (This follows the same convention as the Dioxus framework's distinction between `ssr` for server-side rendering, -/// and `csr` and `hydrate` for client-side rendering and hydration, respectively.) -/// -/// Server functions are created using the `server` macro. -/// -/// The function should be registered by calling `ServerFn::register()`. The set of server functions -/// can be queried on the server for routing purposes by calling [server_fn_by_path]. -/// -/// Technically, the trait is implemented on a type that describes the server function's arguments. -pub trait ServerFn: server_fn::ServerFn { - /// Registers the server function, allowing the server to query it by URL. - #[cfg(any(feature = "ssr", doc))] - fn register() -> Result<(), server_fn::ServerFnError> { - Self::register_in::() - } -} - -impl ServerFn for T where T: server_fn::ServerFn {} diff --git a/packages/server/src/server_fn.rs b/packages/server/src/server_fn.rs new file mode 100644 index 000000000..28c7cd9b6 --- /dev/null +++ b/packages/server/src/server_fn.rs @@ -0,0 +1,98 @@ +pub struct DioxusServerContext {} + +#[cfg(any(feature = "ssr", doc))] +pub type ServerFnTraitObj = server_fn::ServerFnTraitObj; + +#[cfg(any(feature = "ssr", doc))] +#[allow(clippy::type_complexity)] +static REGISTERED_SERVER_FUNCTIONS: once_cell::sync::Lazy< + std::sync::Arc< + std::sync::RwLock< + std::collections::HashMap<&'static str, std::sync::Arc>, + >, + >, +> = once_cell::sync::Lazy::new(Default::default); + +#[cfg(any(feature = "ssr", doc))] +/// The registry of all Dioxus server functions. +pub struct DioxusServerFnRegistry; + +#[cfg(any(feature = "ssr"))] +impl server_fn::ServerFunctionRegistry for DioxusServerFnRegistry { + type Error = ServerRegistrationFnError; + + fn register( + url: &'static str, + server_function: std::sync::Arc, + ) -> Result<(), Self::Error> { + // store it in the hashmap + let mut write = REGISTERED_SERVER_FUNCTIONS + .write() + .map_err(|e| ServerRegistrationFnError::Poisoned(e.to_string()))?; + let prev = write.insert(url, server_function); + + // if there was already a server function with this key, + // return Err + match prev { + Some(_) => Err(ServerRegistrationFnError::AlreadyRegistered(format!( + "There was already a server function registered at {:?}. \ + This can happen if you use the same server function name \ + in two different modules + on `stable` or in `release` mode.", + url + ))), + None => Ok(()), + } + } + + /// Returns the server function registered at the given URL, or `None` if no function is registered at that URL. + fn get(url: &str) -> Option> { + REGISTERED_SERVER_FUNCTIONS + .read() + .ok() + .and_then(|fns| fns.get(url).cloned()) + } + + /// Returns a list of all registered server functions. + fn paths_registered() -> Vec<&'static str> { + REGISTERED_SERVER_FUNCTIONS + .read() + .ok() + .map(|fns| fns.keys().cloned().collect()) + .unwrap_or_default() + } +} + +#[cfg(any(feature = "ssr", doc))] +/// Errors that can occur when registering a server function. +#[derive(thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize)] +pub enum ServerRegistrationFnError { + /// The server function is already registered. + #[error("The server function {0} is already registered")] + AlreadyRegistered(String), + /// The server function registry is poisoned. + #[error("The server function registry is poisoned: {0}")] + Poisoned(String), +} + +/// Defines a "server function." A server function can be called from the server or the client, +/// but the body of its code will only be run on the server, i.e., if a crate feature `ssr` is enabled. +/// +/// (This follows the same convention as the Dioxus framework's distinction between `ssr` for server-side rendering, +/// and `csr` and `hydrate` for client-side rendering and hydration, respectively.) +/// +/// Server functions are created using the `server` macro. +/// +/// The function should be registered by calling `ServerFn::register()`. The set of server functions +/// can be queried on the server for routing purposes by calling [server_fn_by_path]. +/// +/// Technically, the trait is implemented on a type that describes the server function's arguments. +pub trait ServerFn: server_fn::ServerFn { + /// Registers the server function, allowing the server to query it by URL. + #[cfg(any(feature = "ssr", doc))] + fn register() -> Result<(), server_fn::ServerFnError> { + Self::register_in::() + } +} + +impl ServerFn for T where T: server_fn::ServerFn {}