diff --git a/packages/desktop/src/launch.rs b/packages/desktop/src/launch.rs index d32070140..fc848d63f 100644 --- a/packages/desktop/src/launch.rs +++ b/packages/desktop/src/launch.rs @@ -11,7 +11,7 @@ use tao::event::{Event, StartCause, WindowEvent}; /// /// This will block the main thread, and *must* be spawned on the main thread. This function does not assume any runtime /// and is equivalent to calling launch_with_props with the tokio feature disabled. -pub fn launch_with_props_blocking(virtual_dom: VirtualDom, desktop_config: Config) { +pub fn launch_virtual_dom_blocking(virtual_dom: VirtualDom, desktop_config: Config) { let (event_loop, mut app) = App::new(desktop_config, virtual_dom); event_loop.run(move |window_event, _, control_flow| { @@ -48,6 +48,21 @@ pub fn launch_with_props_blocking(virtual_dom: VirtualDom, desktop_config: Confi }) } +/// Launches the WebView and runs the event loop, with configuration and root props. +pub fn launch_virtual_dom(virtual_dom: VirtualDom, desktop_config: Config) { + #[cfg(feature = "tokio")] + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap() + .block_on(tokio::task::unconstrained(async move { + launch_virtual_dom_blocking(virtual_dom, desktop_config) + })); + + #[cfg(not(feature = "tokio"))] + launch_virtual_dom_blocking(config, desktop_config) +} + /// Launches the WebView and runs the event loop, with configuration and root props. pub fn launch( root: fn() -> Element, diff --git a/packages/fullstack/src/adapters/axum_adapter.rs b/packages/fullstack/src/adapters/axum_adapter.rs index b96fe4b81..39fcba570 100644 --- a/packages/fullstack/src/adapters/axum_adapter.rs +++ b/packages/fullstack/src/adapters/axum_adapter.rs @@ -63,7 +63,8 @@ use axum::{ routing::{get, post}, Router, }; -use dioxus_lib::prelude::dioxus_core::{AnyProps, CrossPlatformConfig}; +use dioxus_lib::prelude::dioxus_core::AnyProps; +use dioxus_lib::prelude::VirtualDom; use server_fn::{Encoding, ServerFunctionRegistry}; use std::sync::Arc; use std::sync::RwLock; @@ -216,11 +217,11 @@ pub trait DioxusRouterExt { /// todo!() /// } /// ``` - fn serve_dioxus_application( + fn serve_dioxus_application( self, server_fn_route: &'static str, cfg: impl Into, - dioxus_config: CrossPlatformConfig

, + build_virtual_dom: impl Fn() -> VirtualDom + Send + Sync + 'static, ) -> Self; } @@ -315,11 +316,11 @@ where self } - fn serve_dioxus_application( + fn serve_dioxus_application( self, server_fn_route: &'static str, cfg: impl Into, - dioxus_config: CrossPlatformConfig

, + build_virtual_dom: impl Fn() -> VirtualDom + Send + Sync + 'static, ) -> Self { let cfg = cfg.into(); let ssr_state = SSRState::new(&cfg); @@ -328,7 +329,7 @@ where self.serve_static_assets(cfg.assets_path) .connect_hot_reload() .register_server_fns(server_fn_route) - .fallback(get(render_handler).with_state((cfg, dioxus_config, ssr_state))) + .fallback(get(render_handler).with_state((cfg, Arc::new(build_virtual_dom), ssr_state))) } fn connect_hot_reload(self) -> Self { @@ -418,15 +419,12 @@ fn apply_request_parts_to_response( /// .unwrap(); /// } /// ``` -pub async fn render_handler_with_context< - P: AnyProps + Clone + Send + Sync + 'static, - F: FnMut(&mut DioxusServerContext), ->( - State((mut inject_context, cfg, ssr_state, dioxus_config)): State<( +pub async fn render_handler_with_context( + State((mut inject_context, cfg, ssr_state, virtual_dom_factory)): State<( F, ServeConfig, SSRState, - CrossPlatformConfig

, + Arc VirtualDom + Send + Sync>, )>, request: Request, ) -> impl IntoResponse { @@ -437,7 +435,7 @@ pub async fn render_handler_with_context< inject_context(&mut server_context); match ssr_state - .render(url, &cfg, dioxus_config, &server_context) + .render(url, &cfg, move || virtual_dom_factory(), &server_context) .await { Ok(rendered) => { @@ -456,12 +454,16 @@ pub async fn render_handler_with_context< } /// SSR renderer handler for Axum -pub async fn render_handler( - State((cfg, dioxus_config, ssr_state)): State<(ServeConfig, CrossPlatformConfig

, SSRState)>, +pub async fn render_handler( + State((cfg, virtual_dom_factory, ssr_state)): State<( + ServeConfig, + Arc VirtualDom + Send + Sync>, + SSRState, + )>, request: Request, ) -> impl IntoResponse { render_handler_with_context( - State((|_: &mut _| (), cfg, ssr_state, dioxus_config)), + State((|_: &mut _| (), cfg, ssr_state, virtual_dom_factory)), request, ) .await diff --git a/packages/fullstack/src/config.rs b/packages/fullstack/src/config.rs new file mode 100644 index 000000000..8107b1ace --- /dev/null +++ b/packages/fullstack/src/config.rs @@ -0,0 +1,206 @@ +//! Launch helper macros for fullstack apps +#![allow(unused)] +use crate::prelude::*; +use dioxus_lib::prelude::{dioxus_core::AnyProps, *}; +use std::sync::Arc; + +/// Settings for a fullstack app. +pub struct Config { + #[cfg(feature = "ssr")] + server_fn_route: &'static str, + #[cfg(feature = "ssr")] + server_cfg: ServeConfigBuilder, + #[cfg(feature = "ssr")] + addr: std::net::SocketAddr, + #[cfg(feature = "web")] + web_cfg: dioxus_web::Config, + #[cfg(feature = "desktop")] + desktop_cfg: dioxus_desktop::Config, +} + +#[allow(clippy::derivable_impls)] +impl Default for Config { + fn default() -> Self { + Self { + #[cfg(feature = "ssr")] + server_fn_route: "", + #[cfg(feature = "ssr")] + addr: std::net::SocketAddr::from(([127, 0, 0, 1], 8080)), + #[cfg(feature = "ssr")] + server_cfg: ServeConfigBuilder::new(), + #[cfg(feature = "web")] + web_cfg: dioxus_web::Config::default(), + #[cfg(feature = "desktop")] + desktop_cfg: dioxus_desktop::Config::default(), + } + } +} + +impl Config { + /// Create a new config for a fullstack app. + pub fn new() -> Self { + Self::default() + } + + /// Set the address to serve the app on. + #[cfg(feature = "ssr")] + pub fn addr(self, addr: impl Into) -> Self { + let addr = addr.into(); + Self { addr, ..self } + } + + /// Set the route to the server functions. + #[cfg(feature = "ssr")] + pub fn server_fn_route(self, server_fn_route: &'static str) -> Self { + Self { + server_fn_route, + ..self + } + } + + /// Set the incremental renderer config. + #[cfg(feature = "ssr")] + pub fn incremental(self, cfg: IncrementalRendererConfig) -> Self { + Self { + server_cfg: self.server_cfg.incremental(cfg), + ..self + } + } + + /// Set the server config. + #[cfg(feature = "ssr")] + pub fn server_cfg(self, server_cfg: ServeConfigBuilder) -> Self { + Self { server_cfg, ..self } + } + + /// Set the web config. + #[cfg(feature = "web")] + pub fn web_cfg(self, web_cfg: dioxus_web::Config) -> Self { + Self { web_cfg, ..self } + } + + /// Set the desktop config. + #[cfg(feature = "desktop")] + pub fn desktop_cfg(self, desktop_cfg: dioxus_desktop::Config) -> Self { + Self { + desktop_cfg, + ..self + } + } + + /// Launch the app. + pub fn launch(self, build_virtual_dom: impl Fn() -> VirtualDom + Send + Sync + 'static) { + #[cfg(feature = "ssr")] + tokio::runtime::Runtime::new() + .unwrap() + .block_on(async move { + self.launch_server(build_virtual_dom).await; + }); + #[cfg(not(feature = "ssr"))] + { + #[cfg(feature = "web")] + self.launch_web(build_virtual_dom); + #[cfg(feature = "desktop")] + self.launch_desktop(build_virtual_dom); + } + } + + #[cfg(feature = "web")] + /// Launch the web application + pub fn launch_web(self, build_virtual_dom: impl Fn() -> VirtualDom + Send + Sync + 'static) { + #[cfg(not(feature = "ssr"))] + { + let cfg = self.web_cfg.hydrate(true); + dioxus_web::run( + // TODO: this should pull the props from the document + build_virtual_dom(), + cfg, + ); + } + } + + #[cfg(feature = "desktop")] + /// Launch the web application + pub fn launch_desktop(self, build_virtual_dom: impl Fn() -> VirtualDom + 'static) { + let cfg = self.desktop_cfg; + dioxus_desktop::launch::launch_with_props_blocking(self.component, self.props, cfg); + } + + #[cfg(feature = "ssr")] + /// Launch a server application + pub async fn launch_server( + self, + build_virtual_dom: impl Fn() -> VirtualDom + Send + Sync + 'static, + ) { + let addr = self.addr; + println!("Listening on {}", addr); + let cfg = self.server_cfg.build(); + let server_fn_route = self.server_fn_route; + #[cfg(all(feature = "axum", not(feature = "warp"), not(feature = "salvo")))] + { + use crate::adapters::axum_adapter::{render_handler, DioxusRouterExt}; + use axum::routing::get; + use tower::ServiceBuilder; + + let ssr_state = SSRState::new(&cfg); + let router = axum::Router::new().register_server_fns(server_fn_route); + #[cfg(not(feature = "desktop"))] + let router = router + .serve_static_assets(cfg.assets_path) + .connect_hot_reload() + .fallback(get(render_handler).with_state(( + cfg, + Arc::new(build_virtual_dom), + ssr_state, + ))); + let router = router + .layer( + ServiceBuilder::new() + .layer(tower_http::compression::CompressionLayer::new().gzip(true)), + ) + .into_make_service(); + axum::Server::bind(&addr).serve(router).await.unwrap(); + } + #[cfg(all(feature = "warp", not(feature = "axum"), not(feature = "salvo")))] + { + use warp::Filter; + // First register the server functions + let router = register_server_fns(server_fn_route); + #[cfg(not(feature = "desktop"))] + let router = { + // Serve the dist folder and the index.html file + let serve_dir = warp::fs::dir(cfg.assets_path); + + router + .or(connect_hot_reload()) + // Then the index route + .or(warp::path::end().and(render_ssr(cfg.clone()))) + // Then the static assets + .or(serve_dir) + // Then all other routes + .or(render_ssr(cfg)) + }; + warp::serve(router.boxed().with(warp::filters::compression::gzip())) + .run(addr) + .await; + } + #[cfg(all(feature = "salvo", not(feature = "axum"), not(feature = "warp")))] + { + use crate::adapters::salvo_adapter::{DioxusRouterExt, SSRHandler}; + use salvo::conn::Listener; + let router = salvo::Router::new().register_server_fns(server_fn_route); + #[cfg(not(feature = "desktop"))] + let router = router + .serve_static_assets(cfg.assets_path) + .connect_hot_reload() + .push(salvo::Router::with_path("/<**any_path>").get(SSRHandler::new(cfg))); + let router = router.hoop( + salvo::compression::Compression::new() + .enable_gzip(salvo::prelude::CompressionLevel::Default), + ); + salvo::Server::new(salvo::conn::tcp::TcpListener::new(addr).bind().await) + .serve(router) + .await; + } + } +} diff --git a/packages/fullstack/src/launch.rs b/packages/fullstack/src/launch.rs index 1ca304a91..21092163d 100644 --- a/packages/fullstack/src/launch.rs +++ b/packages/fullstack/src/launch.rs @@ -1,232 +1,35 @@ -//! Launch helper macros for fullstack apps -#![allow(unused)] -use crate::prelude::*; -use dioxus_lib::prelude::{ - dioxus_core::{AnyProps, CrossPlatformConfig}, - *, -}; +//! This module contains the `launch` function, which is the main entry point for dioxus fullstack -pub mod launch { - use dioxus_lib::prelude::{dioxus_core::BoxedContext, Element, VirtualDom}; +use std::any::Any; - pub type Config = crate::Config; +use dioxus_lib::prelude::{Element, VirtualDom}; - pub fn launch( - root: fn() -> Element, - contexts: Vec Box + Send>>, - platform_config: Config, - ) { - #[cfg(feature = "ssr")] - tokio::runtime::Runtime::new() - .unwrap() - .block_on(async move { - platform_config.launch_server(config).await; - }); - #[cfg(not(feature = "ssr"))] - { - #[cfg(feature = "web")] - platform_config.launch_web(dom); - #[cfg(feature = "desktop")] - platform_config.launch_desktop(dom); - } - } -} - -/// Settings for a fullstack app. -pub struct Config { - #[cfg(feature = "ssr")] - server_fn_route: &'static str, - #[cfg(feature = "ssr")] - server_cfg: ServeConfigBuilder, - #[cfg(feature = "ssr")] - addr: std::net::SocketAddr, - #[cfg(feature = "web")] - web_cfg: dioxus_web::Config, - #[cfg(feature = "desktop")] - desktop_cfg: dioxus_desktop::Config, -} - -#[allow(clippy::derivable_impls)] -impl Default for Config { - fn default() -> Self { - Self { - #[cfg(feature = "ssr")] - server_fn_route: "", - #[cfg(feature = "ssr")] - addr: std::net::SocketAddr::from(([127, 0, 0, 1], 8080)), - #[cfg(feature = "ssr")] - server_cfg: ServeConfigBuilder::new(), - #[cfg(feature = "web")] - web_cfg: dioxus_web::Config::default(), - #[cfg(feature = "desktop")] - desktop_cfg: dioxus_desktop::Config::default(), - } - } -} - -impl Config { - /// Create a new config for a fullstack app. - pub fn new() -> Self { - Self::default() - } - - /// Set the address to serve the app on. - #[cfg(feature = "ssr")] - pub fn addr(self, addr: impl Into) -> Self { - let addr = addr.into(); - Self { addr, ..self } - } - - /// Set the route to the server functions. - #[cfg(feature = "ssr")] - pub fn server_fn_route(self, server_fn_route: &'static str) -> Self { - Self { - server_fn_route, - ..self - } - } - - /// Set the incremental renderer config. - #[cfg(feature = "ssr")] - pub fn incremental(self, cfg: IncrementalRendererConfig) -> Self { - Self { - server_cfg: self.server_cfg.incremental(cfg), - ..self - } - } - - /// Set the server config. - #[cfg(feature = "ssr")] - pub fn server_cfg(self, server_cfg: ServeConfigBuilder) -> Self { - Self { server_cfg, ..self } - } - - /// Set the web config. - #[cfg(feature = "web")] - pub fn web_cfg(self, web_cfg: dioxus_web::Config) -> Self { - Self { web_cfg, ..self } - } - - /// Set the desktop config. - #[cfg(feature = "desktop")] - pub fn desktop_cfg(self, desktop_cfg: dioxus_desktop::Config) -> Self { - Self { - desktop_cfg, - ..self - } - } - - /// Launch the app. - pub fn launch(self, dioxus_config: CrossPlatformConfig

) { - #[cfg(feature = "ssr")] - tokio::runtime::Runtime::new() - .unwrap() - .block_on(async move { - self.launch_server(dioxus_config).await; - }); - #[cfg(not(feature = "ssr"))] - { - #[cfg(feature = "web")] - self.launch_web(dioxus_config); - #[cfg(feature = "desktop")] - self.launch_desktop(dioxus_config); - } - } - - #[cfg(feature = "web")] - /// Launch the web application - pub fn launch_web(self, dioxus_config: CrossPlatformConfig

) { - use dioxus_lib::prelude::dioxus_core::{CrossPlatformConfig, PlatformBuilder}; - - #[cfg(not(feature = "ssr"))] - { - let cfg = self.web_cfg.hydrate(true); - dioxus_web::WebPlatform::launch( - // TODO: this should pull the props from the document - dioxus_config, - cfg, - ); - } - } - - #[cfg(feature = "desktop")] - /// Launch the web application - pub fn launch_desktop(self, dioxus_config: CrossPlatformConfig

) { - let cfg = self.desktop_cfg; - dioxus_desktop::launch_with_props(self.component, self.props, cfg); - } - - #[cfg(feature = "ssr")] - /// Launch a server application - pub async fn launch_server( - self, - dioxus_config: CrossPlatformConfig

, - ) { - let addr = self.addr; - println!("Listening on {}", addr); - let cfg = self.server_cfg.build(); - let server_fn_route = self.server_fn_route; - #[cfg(all(feature = "axum", not(feature = "warp"), not(feature = "salvo")))] - { - use crate::adapters::axum_adapter::{render_handler, DioxusRouterExt}; - use axum::routing::get; - use tower::ServiceBuilder; - - let ssr_state = SSRState::new(&cfg); - let router = axum::Router::new().register_server_fns(server_fn_route); - #[cfg(not(feature = "desktop"))] - let router = router - .serve_static_assets(cfg.assets_path) - .connect_hot_reload() - .fallback(get(render_handler).with_state((cfg, dioxus_config, ssr_state))); - let router = router - .layer( - ServiceBuilder::new() - .layer(tower_http::compression::CompressionLayer::new().gzip(true)), - ) - .into_make_service(); - axum::Server::bind(&addr).serve(router).await.unwrap(); - } - #[cfg(all(feature = "warp", not(feature = "axum"), not(feature = "salvo")))] - { - use warp::Filter; - // First register the server functions - let router = register_server_fns(server_fn_route); - #[cfg(not(feature = "desktop"))] - let router = { - // Serve the dist folder and the index.html file - let serve_dir = warp::fs::dir(cfg.assets_path); - - router - .or(connect_hot_reload()) - // Then the index route - .or(warp::path::end().and(render_ssr(cfg.clone()))) - // Then the static assets - .or(serve_dir) - // Then all other routes - .or(render_ssr(cfg)) - }; - warp::serve(router.boxed().with(warp::filters::compression::gzip())) - .run(addr) - .await; - } - #[cfg(all(feature = "salvo", not(feature = "axum"), not(feature = "warp")))] - { - use crate::adapters::salvo_adapter::{DioxusRouterExt, SSRHandler}; - use salvo::conn::Listener; - let router = salvo::Router::new().register_server_fns(server_fn_route); - #[cfg(not(feature = "desktop"))] - let router = router - .serve_static_assets(cfg.assets_path) - .connect_hot_reload() - .push(salvo::Router::with_path("/<**any_path>").get(SSRHandler::new(cfg))); - let router = router.hoop( - salvo::compression::Compression::new() - .enable_gzip(salvo::prelude::CompressionLevel::Default), - ); - salvo::Server::new(salvo::conn::tcp::TcpListener::new(addr).bind().await) - .serve(router) - .await; +pub use crate::Config; + +/// Launch a fullstack app with the given root component, contexts, and config. +pub fn launch( + root: fn() -> Element, + contexts: Vec Box + Send + Sync>>, + platform_config: Config, +) { + let virtual_dom_factory = move || { + let mut vdom = VirtualDom::new(root); + for context in &contexts { + vdom.insert_any_root_context(context()); } + vdom + }; + #[cfg(feature = "ssr")] + tokio::runtime::Runtime::new() + .unwrap() + .block_on(async move { + platform_config.launch_server(virtual_dom_factory).await; + }); + #[cfg(not(feature = "ssr"))] + { + #[cfg(feature = "web")] + platform_config.launch_web(virtual_dom_factory); + #[cfg(feature = "desktop")] + platform_config.launch_desktop(virtual_dom_factory); } } diff --git a/packages/fullstack/src/lib.rs b/packages/fullstack/src/lib.rs index 949320ad6..f0db380cb 100644 --- a/packages/fullstack/src/lib.rs +++ b/packages/fullstack/src/lib.rs @@ -11,11 +11,12 @@ mod html_storage; mod adapters; #[cfg(feature = "ssr")] pub use adapters::*; +mod config; mod hooks; #[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))] mod hot_reload; -mod launch; -pub use launch::*; +pub mod launch; +pub use config::*; #[cfg(feature = "ssr")] mod layer; #[cfg(feature = "ssr")] diff --git a/packages/fullstack/src/render.rs b/packages/fullstack/src/render.rs index e05e4c1d5..83cfaa1ed 100644 --- a/packages/fullstack/src/render.rs +++ b/packages/fullstack/src/render.rs @@ -1,6 +1,5 @@ //! A shared pool of renderers for efficient server side rendering. use crate::render::dioxus_core::AnyProps; -use crate::render::dioxus_core::CrossPlatformConfig; use crate::render::dioxus_core::NoOpMutations; use crate::server_context::SERVER_CONTEXT; use dioxus_lib::prelude::VirtualDom; @@ -22,11 +21,11 @@ enum SsrRendererPool { } impl SsrRendererPool { - async fn render_to( + async fn render_to( &self, cfg: &ServeConfig, route: String, - dioxus_config: CrossPlatformConfig

, + virtual_dom_factory: impl FnOnce() -> VirtualDom + Send + Sync + 'static, server_context: &DioxusServerContext, ) -> Result<(RenderFreshness, String), dioxus_ssr::incremental::IncrementalRendererError> { let wrapper = FullstackRenderer { @@ -45,7 +44,7 @@ impl SsrRendererPool { tokio::runtime::Runtime::new() .expect("couldn't spawn runtime") .block_on(async move { - let mut vdom = dioxus_config.build_vdom(); + let mut vdom = virtual_dom_factory(); vdom.in_runtime(|| { // Make sure the evaluator is initialized dioxus_ssr::eval::init_eval(); @@ -112,7 +111,7 @@ impl SsrRendererPool { match renderer .render( route, - dioxus_config, + virtual_dom_factory, &mut *to, |vdom| { Box::pin(async move { @@ -192,11 +191,11 @@ impl SSRState { } /// Render the application to HTML. - pub fn render<'a, P: AnyProps + Clone + Send + Sync>( + pub fn render<'a>( &'a self, route: String, cfg: &'a ServeConfig, - dioxus_config: CrossPlatformConfig

, + virtual_dom_factory: impl FnOnce() -> VirtualDom + Send + Sync + 'static, server_context: &'a DioxusServerContext, ) -> impl std::future::Future< Output = Result, @@ -207,7 +206,7 @@ impl SSRState { let (freshness, html) = self .renderers - .render_to(cfg, route, dioxus_config, server_context) + .render_to(cfg, route, virtual_dom_factory, server_context) .await?; Ok(RenderResponse { html, freshness }) diff --git a/packages/ssr/src/incremental.rs b/packages/ssr/src/incremental.rs index 72077eeb0..3a17fd959 100644 --- a/packages/ssr/src/incremental.rs +++ b/packages/ssr/src/incremental.rs @@ -170,7 +170,7 @@ impl IncrementalRenderer { pub async fn render( &mut self, route: String, - virtual_dom: VirtualDom, + virtual_dom_factory: impl FnOnce() -> VirtualDom, output: &mut (impl AsyncWrite + Unpin + std::marker::Send), rebuild_with: impl FnOnce(&mut VirtualDom) -> Pin + '_>>, renderer: &R, @@ -181,7 +181,7 @@ impl IncrementalRenderer { } else { // if not, create it let freshness = self - .render_and_cache(route, virtual_dom, output, rebuild_with, renderer) + .render_and_cache(route, virtual_dom_factory(), output, rebuild_with, renderer) .await?; tracing::trace!("cache miss"); Ok(freshness)