Pārlūkot izejas kodu

router/fullstack/ssr intigration

Evan Almloff 2 gadi atpakaļ
vecāks
revīzija
1704ee0068

+ 3 - 3
packages/fullstack/Cargo.toml

@@ -30,13 +30,13 @@ salvo = { version = "0.37.7", optional = true, features = ["serve-static", "ws"]
 serde = "1.0.159"
 
 # Dioxus + SSR
-dioxus-core = { path = "../core", version = "^0.3.0" }
+dioxus = { path = "../dioxus", version = "^0.3.0" }
 dioxus-ssr = { path = "../ssr", version = "^0.3.0", optional = true }
 hyper = { version = "0.14.25", optional = true }
 http = { version = "0.2.9", optional = true }
 
 # Router Intigration
-dioxus-router = { path = "../router", version = "^0.3.0", optional = true }
+dioxus-router = { path = "../router", version = "^0.3.0", features = ["ssr"], optional = true }
 
 log = "0.4.17"
 once_cell = "1.17.1"
@@ -59,7 +59,7 @@ dioxus-hot-reload = { path = "../hot-reload" }
 web-sys = { version = "0.3.61", features = ["Window", "Document", "Element", "HtmlDocument", "Storage", "console"] }
 
 [features]
-default = ["hot-reload", "default-tls"]
+default = ["hot-reload", "default-tls", "router"]
 router = ["dioxus-router"]
 hot-reload = ["serde_json", "tokio-stream", "futures-util"]
 warp = ["dep:warp", "http-body", "ssr"]

+ 10 - 58
packages/fullstack/examples/axum-router/src/main.rs

@@ -14,8 +14,8 @@ use serde::{Deserialize, Serialize};
 fn main() {
     #[cfg(feature = "web")]
     dioxus_web::launch_with_props(
-        App,
-        AppProps { route: None },
+        Router,
+        Default::default(),
         dioxus_web::Config::new().hydrate(true),
     );
     #[cfg(feature = "ssr")]
@@ -33,11 +33,6 @@ fn main() {
             true
         }));
 
-        use axum::extract::State;
-        use std::str::FromStr;
-
-        PostServerData::register().unwrap();
-        GetServerData::register().unwrap();
         tokio::runtime::Runtime::new()
             .unwrap()
             .block_on(async move {
@@ -46,28 +41,12 @@ fn main() {
                 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: Route::from_str(&uri.to_string()).ok(),
-                                            },
-                                        )
-                                        .build(),
-                                    );
-                                    async move { axum::body::Full::from(rendered) }
-                                },
+                            .serve_dioxus_application(
+                                "",
+                                ServeConfigBuilder::new_with_router(
+                                    dioxus_fullstack::prelude::FullstackRouterConfig::<Route>::default()).incremental(IncrementalRendererConfig::default())
+                                .build(),
                             )
-                            .with_state(SSRState::default())
                             .into_make_service(),
                     )
                     .await
@@ -87,10 +66,7 @@ enum Route {
 #[inline_props]
 fn Blog(cx: Scope) -> Element {
     render! {
-        Link {
-            target: Route::Home {},
-            "Go to counter"
-        }
+        Link { target: Route::Home {}, "Go to counter" }
         table {
             tbody {
                 for _ in 0..100 {
@@ -105,38 +81,14 @@ fn Blog(cx: Scope) -> Element {
     }
 }
 
-#[derive(Clone, Debug, Props, PartialEq, Serialize, Deserialize)]
-struct AppProps {
-    route: Option<Route>,
-}
-
-fn App(cx: Scope<AppProps>) -> Element {
-    #[cfg(feature = "ssr")]
-    let initial_route = cx.props.route.clone().unwrap_or(Route::Home {});
-    cx.render(rsx! {
-        Router {
-            config: move || RouterConfig::default().history({
-                #[cfg(feature = "ssr")]
-                let history = MemoryHistory::with_initial_path(initial_route);
-                #[cfg(feature = "web")]
-                let history = WebHistory::default();
-                history
-            })
-        }
-    })
-}
-
 #[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{
+        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!" }

+ 12 - 10
packages/fullstack/src/adapters/axum_adapter.rs

@@ -63,7 +63,6 @@ use axum::{
     routing::{get, post},
     Router,
 };
-use dioxus_core::VirtualDom;
 use server_fn::{Encoding, Payload, ServerFunctionRegistry};
 use std::error::Error;
 use std::sync::Arc;
@@ -311,15 +310,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 {
@@ -358,13 +355,18 @@ async fn render_handler<P: Clone + serde::Serialize + Send + Sync + 'static>(
 ) -> impl IntoResponse {
     let (parts, _) = request.into_parts();
     let parts: Arc<RequestParts> = Arc::new(parts.into());
+    let url = parts.uri.path_and_query().unwrap().to_string();
     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 rendered = ssr_state.render_vdom(&vdom, &cfg);
-    Full::from(rendered)
+    match ssr_state.render(url, &cfg, |vdom| {
+        vdom.base_scope().provide_context(server_context);
+    }) {
+        Ok(html) => Full::from(html).into_response(),
+        Err(e) => {
+            log::error!("Failed to render page: {}", e);
+            report_err(e).into_response()
+        }
+    }
 }
 
 /// A default handler for server functions. It will deserialize the request, call the server function, and serialize the response.

+ 1 - 1
packages/fullstack/src/adapters/salvo_adapter.rs

@@ -282,7 +282,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 {

+ 1 - 1
packages/fullstack/src/adapters/warp_adapter.rs

@@ -169,8 +169,8 @@ pub fn serve_dioxus_application<P: Clone + serde::Serialize + Send + Sync + 'sta
 
     connect_hot_reload()
         .or(register_server_fns(server_fn_route))
-        .or(warp::path::end().and(render_ssr(cfg)))
         .or(serve_dir)
+        .or(render_ssr(cfg))
         .boxed()
 }
 

+ 3 - 2
packages/fullstack/src/hot_reload.rs

@@ -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
packages/fullstack/src/incremental.rs


+ 4 - 2
packages/fullstack/src/lib.rs

@@ -10,8 +10,6 @@ mod props_html;
 mod adapters;
 #[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))]
 mod hot_reload;
-#[cfg(feature = "router")]
-mod incremental;
 #[cfg(feature = "ssr")]
 mod render;
 #[cfg(feature = "ssr")]
@@ -31,6 +29,8 @@ pub mod prelude {
     pub use crate::props_html::deserialize_props::get_root_props_from_document;
     #[cfg(feature = "ssr")]
     pub use crate::render::SSRState;
+    #[cfg(all(feature = "router", feature = "ssr"))]
+    pub use crate::serve_config::FullstackRouterConfig;
     #[cfg(feature = "ssr")]
     pub use crate::serve_config::{ServeConfig, ServeConfigBuilder};
     #[cfg(feature = "ssr")]
@@ -40,5 +40,7 @@ pub mod prelude {
     #[cfg(feature = "ssr")]
     pub use crate::server_fn::{ServerFnTraitObj, ServerFunction};
     pub use dioxus_server_macro::*;
+    #[cfg(feature = "ssr")]
+    pub use dioxus_ssr::incremental::IncrementalRendererConfig;
     pub use server_fn::{self, ServerFn as _, ServerFnError};
 }

+ 77 - 27
packages/fullstack/src/render.rs

@@ -2,53 +2,93 @@
 
 use std::sync::Arc;
 
-use dioxus_core::VirtualDom;
-use dioxus_ssr::Renderer;
+use dioxus::prelude::VirtualDom;
+use dioxus_ssr::{incremental::IncrementalRendererConfig, Renderer};
+
+use crate::prelude::*;
+use dioxus::prelude::*;
+
+enum SsrRendererPool {
+    Renderer(object_pool::Pool<Renderer>),
+    Incremental(
+        object_pool::Pool<
+            dioxus_ssr::incremental::IncrementalRenderer<
+                crate::serve_config::EmptyIncrementalRenderTemplate,
+            >,
+        >,
+    ),
+}
 
-use crate::prelude::ServeConfig;
+impl SsrRendererPool {
+    fn render_to<P: Clone + 'static>(
+        &self,
+        cfg: &ServeConfig<P>,
+        route: String,
+        component: Component<P>,
+        props: P,
+        to: &mut String,
+        modify_vdom: impl FnOnce(&mut VirtualDom),
+    ) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> {
+        match self {
+            Self::Renderer(pool) => {
+                let mut vdom = VirtualDom::new_with_props(component, props);
+
+                let _ = vdom.rebuild();
+                let mut renderer = pool.pull(pre_renderer);
+                renderer.render_to(to, &vdom)?;
+            }
+            Self::Incremental(pool) => {
+                let mut renderer = pool.pull(|| incremental_pre_renderer(cfg));
+                renderer.render_to_string(route, component, props, to, modify_vdom)?;
+            }
+        }
+        Ok(())
+    }
+}
 
 /// 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),
+                ))),
+            };
+        }
+
         Self {
-            renderers: Arc::new(object_pool::Pool::new(10, pre_renderer)),
+            renderers: Arc::new(SsrRendererPool::Renderer(object_pool::Pool::new(
+                10,
+                pre_renderer,
+            ))),
         }
     }
-}
 
-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;
-
-        let mut vdom = VirtualDom::new_with_props(*app, props.clone());
-
-        let _ = vdom.rebuild();
-
-        self.render_vdom(&vdom, cfg)
-    }
-
-    /// Render a VirtualDom to HTML.
-    pub fn render_vdom<P: 'static + Clone + serde::Serialize>(
+    pub fn render<P: 'static + Clone + serde::Serialize>(
         &self,
-        vdom: &VirtualDom,
+        route: String,
         cfg: &ServeConfig<P>,
-    ) -> String {
-        let ServeConfig { index, .. } = cfg;
+        modify_vdom: impl FnOnce(&mut VirtualDom),
+    ) -> Result<String, dioxus_ssr::incremental::IncrementalRendererError> {
+        let ServeConfig { app, props, .. } = cfg;
 
-        let mut renderer = self.renderers.pull(pre_renderer);
+        let ServeConfig { index, .. } = cfg;
 
         let mut html = String::new();
 
         html += &index.pre_main;
 
-        let _ = renderer.render_to(&mut html, vdom);
+        self.renderers
+            .render_to(cfg, route, *app, props.clone(), &mut html, modify_vdom)?;
 
         // serialize the props
         let _ = crate::props_html::serialize_props::encode_in_element(&cfg.props, &mut html);
@@ -89,12 +129,22 @@ impl SSRState {
 
         html += &index.post_main;
 
-        html
+        Ok(html)
     }
 }
 
 fn pre_renderer() -> Renderer {
     let mut renderer = Renderer::default();
     renderer.pre_render = true;
+    renderer.into()
+}
+
+fn incremental_pre_renderer<P: Clone>(
+    cfg: &ServeConfig<P>,
+) -> dioxus_ssr::incremental::IncrementalRenderer<crate::serve_config::EmptyIncrementalRenderTemplate>
+{
+    let builder: &IncrementalRendererConfig<_> = &*cfg.incremental.as_ref().unwrap();
+    let mut renderer = builder.clone().build();
+    renderer.renderer_mut().pre_render = true;
     renderer
 }

+ 180 - 1
packages/fullstack/src/serve_config.rs

@@ -1,10 +1,11 @@
+#![allow(non_snake_case)]
 //! Configeration for how to serve a Dioxus application
 
 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 +15,164 @@ 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>,
+    #[cfg(feature = "router")]
+    pub(crate) incremental: Option<
+        std::sync::Arc<
+            dioxus_ssr::incremental::IncrementalRendererConfig<EmptyIncrementalRenderTemplate>,
+        >,
+    >,
+}
+
+#[cfg(feature = "router")]
+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>
+}
+
+#[cfg(feature = "router")]
+/// 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>,
+}
+
+#[cfg(feature = "router")]
+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,
+        }
+    }
+}
+
+#[cfg(feature = "router")]
+impl<R> Copy for FullstackRouterConfig<R>
+where
+    R: dioxus_router::prelude::Routable,
+    <R as std::str::FromStr>::Err: std::fmt::Display,
+{
+}
+
+#[cfg(feature = "router")]
+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,
+        }
+    }
+}
+
+#[cfg(feature = "router")]
+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::DioxusServerContext = cx
+        .consume_context()
+        .expect("RouteWithCfg should be served by dioxus fullstack");
+
+    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()
+                                .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
+                    })
+            },
+        }
+    }
+}
+
+#[cfg(feature = "router")]
+/// A template for incremental rendering that does nothing.
+#[derive(Default, Clone)]
+pub struct EmptyIncrementalRenderTemplate;
+
+#[cfg(feature = "router")]
+impl dioxus_ssr::incremental::RenderHTML 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 {
+            app: RouteWithCfg::<R>,
+            props: cfg,
+            root_id: None,
+            index_path: None,
+            assets_path: None,
+            incremental: None,
+        }
+    }
 }
 
 impl<P: Clone> ServeConfigBuilder<P> {
@@ -25,9 +184,21 @@ impl<P: Clone> ServeConfigBuilder<P> {
             root_id: None,
             index_path: None,
             assets_path: None,
+            #[cfg(feature = "router")]
+            incremental: None,
         }
     }
 
+    #[cfg(feature = "router")]
+    /// Enable incremental static generation
+    pub fn incremental(
+        mut self,
+        cfg: dioxus_ssr::incremental::IncrementalRendererConfig<EmptyIncrementalRenderTemplate>,
+    ) -> 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 +235,8 @@ impl<P: Clone> ServeConfigBuilder<P> {
             props: self.props,
             index,
             assets_path,
+            #[cfg(feature = "router")]
+            incremental: self.incremental,
         }
     }
 }
@@ -106,6 +279,12 @@ pub struct ServeConfig<P: Clone> {
     pub(crate) props: P,
     pub(crate) index: IndexHtml,
     pub(crate) assets_path: &'static str,
+    #[cfg(feature = "router")]
+    pub(crate) incremental: Option<
+        std::sync::Arc<
+            dioxus_ssr::incremental::IncrementalRendererConfig<EmptyIncrementalRenderTemplate>,
+        >,
+    >,
 }
 
 impl<P: Clone> From<ServeConfigBuilder<P>> for ServeConfig<P> {

+ 1 - 1
packages/fullstack/src/server_context.rs

@@ -1,4 +1,4 @@
-use dioxus_core::ScopeState;
+use dioxus::prelude::ScopeState;
 
 /// A trait for an object that contains a server context
 pub trait HasServerContext {

+ 1 - 4
packages/router/Cargo.toml

@@ -24,13 +24,10 @@ web-sys = { version = "0.3.60", optional = true, features = ["ScrollRestoration"
 js-sys = { version = "0.3.63", optional = true }
 gloo-utils = { version = "0.1.6", optional = true }
 dioxus-ssr = { path = "../ssr", optional = true }
-lru = { version = "0.10.0", optional = true }
-radix_trie = { version = "0.2.1", optional = true }
-rustc-hash = "1.1.0"
 
 [features]
 default = ["web", "ssr"]
-ssr = ["dioxus-ssr", "lru", "radix_trie"]
+ssr = ["dioxus-ssr"]
 wasm_test = []
 serde = ["dep:serde", "gloo-utils/serde"]
 web = ["gloo", "web-sys", "wasm-bindgen", "gloo-utils", "js-sys"]

+ 1 - 1
packages/router/benches/incremental.rs

@@ -6,7 +6,7 @@ use dioxus::prelude::*;
 use dioxus_router::prelude::*;
 
 use criterion::{black_box, criterion_group, criterion_main, Criterion};
-use dioxus_router::ssr::{DefaultRenderer, IncrementalRenderer};
+use dioxus_router::incremental::{DefaultRenderer, IncrementalRenderer};
 use dioxus_ssr::Renderer;
 
 pub fn criterion_benchmark(c: &mut Criterion) {

+ 1 - 1
packages/router/examples/static_generation.rs

@@ -5,7 +5,7 @@ use std::time::Duration;
 use dioxus::prelude::*;
 use dioxus_router::prelude::*;
 
-use dioxus_router::ssr::{DefaultRenderer, IncrementalRendererConfig};
+use dioxus_ssr::incremental::{DefaultRenderer, IncrementalRendererConfig};
 
 fn main() {
     let mut renderer = IncrementalRendererConfig::new(DefaultRenderer {

+ 25 - 0
packages/router/src/components/router.rs

@@ -66,6 +66,31 @@ where
     config: RouterConfigFactory<R>,
 }
 
+#[cfg(not(feature = "serde"))]
+impl<R: Routable> Default for GenericRouterProps<R>
+where
+    <R as FromStr>::Err: std::fmt::Display,
+{
+    fn default() -> Self {
+        Self {
+            config: RouterConfigFactory::default(),
+        }
+    }
+}
+
+#[cfg(feature = "serde")]
+impl<R: Routable> Default for GenericRouterProps<R>
+where
+    <R as FromStr>::Err: std::fmt::Display,
+    R: serde::Serialize + serde::de::DeserializeOwned,
+{
+    fn default() -> Self {
+        Self {
+            config: RouterConfigFactory::default(),
+        }
+    }
+}
+
 #[cfg(not(feature = "serde"))]
 impl<R: Routable> PartialEq for GenericRouterProps<R>
 where

+ 104 - 0
packages/router/src/incremental.rs

@@ -0,0 +1,104 @@
+//! Exentsions to the incremental renderer to support pre-caching static routes.
+use std::str::FromStr;
+
+use dioxus::prelude::*;
+use dioxus_ssr::incremental::{IncrementalRenderer, IncrementalRendererError, RenderHTML};
+
+use crate::prelude::*;
+
+trait IncrementalRendererRouterExt {
+    /// Pre-cache all static routes.
+    fn pre_cache_static_routes<Rt>(&mut self) -> Result<(), IncrementalRendererError>
+    where
+        Rt: Routable,
+        <Rt as FromStr>::Err: std::fmt::Display;
+
+    /// Render a route to a writer.
+    fn render_route<Rt, W, F: FnOnce(&mut VirtualDom)>(
+        &mut self,
+        route: Rt,
+        writer: &mut W,
+        modify_vdom: F,
+    ) -> Result<(), IncrementalRendererError>
+    where
+        Rt: Routable,
+        <Rt as FromStr>::Err: std::fmt::Display,
+        W: std::io::Write;
+}
+
+impl<R: RenderHTML> IncrementalRendererRouterExt for IncrementalRenderer<R> {
+    fn pre_cache_static_routes<Rt>(&mut self) -> Result<(), IncrementalRendererError>
+    where
+        Rt: Routable,
+        <Rt as FromStr>::Err: std::fmt::Display,
+    {
+        for route in Rt::SITE_MAP
+            .iter()
+            .flat_map(|seg| seg.flatten().into_iter())
+        {
+            // check if this is a static segment
+            let mut is_static = true;
+            let mut full_path = String::new();
+            for segment in &route {
+                match segment {
+                    SegmentType::Static(s) => {
+                        full_path += "/";
+                        full_path += s;
+                    }
+                    _ => {
+                        // skip routes with any dynamic segments
+                        is_static = false;
+                        break;
+                    }
+                }
+            }
+
+            if is_static {
+                match Rt::from_str(&full_path) {
+                    Ok(route) => {
+                        self.render_route(route, &mut std::io::sink(), |_| {})?;
+                    }
+                    Err(e) => {
+                        log::error!("Error pre-caching static route: {}", e);
+                    }
+                }
+            }
+        }
+
+        Ok(())
+    }
+
+    fn render_route<Rt, W, F: FnOnce(&mut VirtualDom)>(
+        &mut self,
+        route: Rt,
+        writer: &mut W,
+        modify_vdom: F,
+    ) -> Result<(), IncrementalRendererError>
+    where
+        Rt: Routable,
+        <Rt as FromStr>::Err: std::fmt::Display,
+        W: std::io::Write,
+    {
+        #[inline_props]
+        fn RenderPath<R>(cx: Scope, path: R) -> Element
+        where
+            R: Routable,
+            <R as FromStr>::Err: std::fmt::Display,
+        {
+            let path = path.clone();
+            render! {
+                GenericRouter::<R> {
+                    config: || RouterConfig::default().history(MemoryHistory::with_initial_path(path))
+                }
+            }
+        }
+
+        self.render(
+            route.to_string(),
+            RenderPath,
+            RenderPathProps { path: route },
+            writer,
+            modify_vdom,
+        )
+    }
+}

+ 4 - 1
packages/router/src/lib.rs

@@ -7,7 +7,7 @@ pub mod navigation;
 pub mod routable;
 
 #[cfg(feature = "ssr")]
-pub mod ssr;
+pub mod incremental;
 
 /// Components interacting with the router.
 pub mod components {
@@ -62,6 +62,9 @@ pub mod prelude {
     pub use crate::router_cfg::RouterConfig;
     pub use dioxus_router_macro::Routable;
 
+    #[cfg(feature = "ssr")]
+    pub use dioxus_ssr::incremental::IncrementalRendererConfig;
+
     #[doc(hidden)]
     /// A component with props used in the macro
     pub trait HasProps {

+ 3 - 3
packages/router/src/routable.rs

@@ -94,9 +94,9 @@ impl<I: std::iter::FromIterator<String>> FromRouteSegments for I {
 }
 
 /// Something that can be:
-/// 1) Converted from a route
-/// 2) Converted to a route
-/// 3) Rendered as a component
+/// 1. Converted from a route
+/// 2. Converted to a route
+/// 3. Rendered as a component
 ///
 /// This trait can be derived using the `#[derive(Routable)]` macro
 pub trait Routable: std::fmt::Display + std::str::FromStr + Clone + 'static {

+ 4 - 2
packages/ssr/Cargo.toml

@@ -15,11 +15,13 @@ keywords = ["dom", "ui", "gui", "react", "ssr"]
 [dependencies]
 dioxus-core = { path = "../core", version = "^0.3.0", features = ["serialize"] }
 askama_escape = "0.10.3"
+thiserror = "1.0.23"
+rustc-hash = "1.1.0"
+lru = "0.10.0"
+log = "0.4.13"
 
 [dev-dependencies]
 dioxus = { path = "../dioxus", version = "0.3.0" }
-thiserror = "1.0.23"
-log = "0.4.13"
 fern = { version = "0.6.0", features = ["colored"] }
 anyhow = "1.0"
 argh = "0.1.4"

+ 70 - 84
packages/router/src/ssr.rs → packages/ssr/src/incremental.rs

@@ -2,15 +2,13 @@
 
 #![allow(non_snake_case)]
 
-use crate::prelude::*;
-use dioxus::prelude::*;
+use dioxus_core::{Element, Scope, VirtualDom};
 use rustc_hash::FxHasher;
 use std::{
     hash::BuildHasherDefault,
     io::Write,
     num::NonZeroUsize,
     path::{Path, PathBuf},
-    str::FromStr,
     time::{Duration, SystemTime},
 };
 
@@ -62,6 +60,7 @@ impl RenderHTML for DefaultRenderer {
 }
 
 /// A configuration for the incremental renderer.
+#[derive(Debug, Clone)]
 pub struct IncrementalRendererConfig<R: RenderHTML> {
     static_dir: PathBuf,
     memory_cache_limit: usize,
@@ -69,9 +68,9 @@ pub struct IncrementalRendererConfig<R: RenderHTML> {
     render: R,
 }
 
-impl Default for IncrementalRendererConfig<DefaultRenderer> {
+impl<R: RenderHTML + Default> Default for IncrementalRendererConfig<R> {
     fn default() -> Self {
-        Self::new(DefaultRenderer::default())
+        Self::new(R::default())
     }
 }
 
@@ -112,7 +111,7 @@ impl<R: RenderHTML> IncrementalRendererConfig<R> {
                 .map(|limit| lru::LruCache::with_hasher(limit, Default::default())),
             invalidate_after: self.invalidate_after,
             render: self.render,
-            ssr_renderer: dioxus_ssr::Renderer::new(),
+            ssr_renderer: crate::Renderer::new(),
         }
     }
 }
@@ -120,45 +119,74 @@ impl<R: RenderHTML> IncrementalRendererConfig<R> {
 /// An incremental renderer.
 pub struct IncrementalRenderer<R: RenderHTML> {
     static_dir: PathBuf,
+    #[allow(clippy::type_complexity)]
     memory_cache:
         Option<lru::LruCache<String, (SystemTime, Vec<u8>), BuildHasherDefault<FxHasher>>>,
     invalidate_after: Option<Duration>,
-    ssr_renderer: dioxus_ssr::Renderer,
+    ssr_renderer: crate::Renderer,
     render: R,
 }
 
 impl<R: RenderHTML> IncrementalRenderer<R> {
+    /// Get the inner renderer.
+    pub fn renderer(&self) -> &crate::Renderer {
+        &self.ssr_renderer
+    }
+
+    /// Get the inner renderer mutably.
+    pub fn renderer_mut(&mut self) -> &mut crate::Renderer {
+        &mut self.ssr_renderer
+    }
+
     /// Create a new incremental renderer builder.
     pub fn builder(renderer: R) -> IncrementalRendererConfig<R> {
         IncrementalRendererConfig::new(renderer)
     }
 
+    /// Remove a route from the cache.
+    pub fn invalidate(&mut self, route: &str) {
+        if let Some(cache) = &mut self.memory_cache {
+            cache.pop(route);
+        }
+        if let Some(path) = self.find_file(route) {
+            let _ = std::fs::remove_file(path.full_path);
+        }
+    }
+
+    /// Remove all routes from the cache.
+    pub fn invalidate_all(&mut self) {
+        if let Some(cache) = &mut self.memory_cache {
+            cache.clear();
+        }
+        // clear the static directory
+        let _ = std::fs::remove_dir_all(&self.static_dir);
+    }
+
     fn track_timestamps(&self) -> bool {
         self.invalidate_after.is_some()
     }
 
-    fn render_and_cache<Rt>(
+    fn render_and_cache<P: 'static>(
         &mut self,
-        route: Rt,
+        route: String,
+        comp: fn(Scope<P>) -> Element,
+        props: P,
         output: &mut impl Write,
-    ) -> Result<(), IncrementalRendererError>
-    where
-        Rt: Routable,
-        <Rt as FromStr>::Err: std::fmt::Display,
-    {
-        let route_str = route.to_string();
-        let mut vdom = VirtualDom::new_with_props(RenderPath, RenderPathProps { path: route });
+        modify_vdom: impl FnOnce(&mut VirtualDom),
+    ) -> Result<(), IncrementalRendererError> {
+        let mut vdom = VirtualDom::new_with_props(comp, props);
+        modify_vdom(&mut vdom);
         let _ = vdom.rebuild();
 
         let mut html_buffer = WriteBuffer { buffer: Vec::new() };
         self.render.render_before_body(&mut html_buffer)?;
-        self.ssr_renderer.render_to(&mut html_buffer, &mut vdom)?;
+        self.ssr_renderer.render_to(&mut html_buffer, &vdom)?;
         self.render.render_after_body(&mut html_buffer)?;
         let html_buffer = html_buffer.buffer;
 
         output.write_all(&html_buffer)?;
 
-        self.add_to_cache(route_str, html_buffer)
+        self.add_to_cache(route, html_buffer)
     }
 
     fn add_to_cache(
@@ -181,7 +209,7 @@ impl<R: RenderHTML> IncrementalRenderer<R> {
 
     fn add_to_memory_cache(&mut self, route: String, html: Vec<u8>) {
         if let Some(cache) = self.memory_cache.as_mut() {
-            cache.put(route.to_string(), (SystemTime::now(), html));
+            cache.put(route, (SystemTime::now(), html));
         }
     }
 
@@ -228,25 +256,39 @@ impl<R: RenderHTML> IncrementalRenderer<R> {
     }
 
     /// Render a route or get it from cache.
-    pub fn render<Rt>(
+    pub fn render<P: 'static>(
         &mut self,
-        route: Rt,
+        route: String,
+        component: fn(Scope<P>) -> Element,
+        props: P,
         output: &mut impl Write,
-    ) -> Result<(), IncrementalRendererError>
-    where
-        Rt: Routable,
-        <Rt as FromStr>::Err: std::fmt::Display,
-    {
+        modify_vdom: impl FnOnce(&mut VirtualDom),
+    ) -> Result<(), IncrementalRendererError> {
         // check if this route is cached
         if !self.search_cache(route.to_string(), output)? {
             // if not, create it
-            self.render_and_cache(route, output)?;
+            self.render_and_cache(route, component, props, output, modify_vdom)?;
             log::trace!("cache miss");
         }
 
         Ok(())
     }
 
+    /// Render a route or get it from cache to a string.
+    pub fn render_to_string<P: 'static>(
+        &mut self,
+        route: String,
+        component: fn(Scope<P>) -> Element,
+        props: P,
+        output: &mut String,
+        modify_vdom: impl FnOnce(&mut VirtualDom),
+    ) -> Result<(), IncrementalRendererError> {
+        unsafe {
+            // SAFETY: The renderer will only write utf8 to the buffer
+            self.render(route, component, props, output.as_mut_vec(), modify_vdom)
+        }
+    }
+
     fn find_file(&self, route: &str) -> Option<ValidCachedPath> {
         let mut file_path = self.static_dir.clone();
         for segment in route.split('/') {
@@ -300,48 +342,6 @@ impl<R: RenderHTML> IncrementalRenderer<R> {
         file_path.set_extension("html");
         file_path
     }
-
-    /// Pre-cache all static routes.
-    pub fn pre_cache_static_routes<Rt>(&mut self) -> Result<(), IncrementalRendererError>
-    where
-        Rt: Routable,
-        <Rt as FromStr>::Err: std::fmt::Display,
-    {
-        for route in Rt::SITE_MAP
-            .iter()
-            .flat_map(|seg| seg.flatten().into_iter())
-        {
-            // check if this is a static segment
-            let mut is_static = true;
-            let mut full_path = String::new();
-            for segment in &route {
-                match segment {
-                    SegmentType::Static(s) => {
-                        full_path += "/";
-                        full_path += s;
-                    }
-                    _ => {
-                        // skip routes with any dynamic segments
-                        is_static = false;
-                        break;
-                    }
-                }
-            }
-
-            if is_static {
-                match Rt::from_str(&full_path) {
-                    Ok(route) => {
-                        let _ = self.render(route, &mut std::io::sink())?;
-                    }
-                    Err(e) => {
-                        log::error!("Error pre-caching static route: {}", e);
-                    }
-                }
-            }
-        }
-
-        Ok(())
-    }
 }
 
 struct WriteBuffer {
@@ -375,7 +375,7 @@ impl ValidCachedPath {
         if value.extension() != Some(std::ffi::OsStr::new("html")) {
             return None;
         }
-        let timestamp = decode_timestamp(&value.file_stem()?.to_str()?)?;
+        let timestamp = decode_timestamp(value.file_stem()?.to_str()?)?;
         let full_path = value;
         Some(Self {
             full_path,
@@ -398,20 +398,6 @@ fn timestamp() -> String {
     format!("{:x}", timestamp)
 }
 
-#[inline_props]
-fn RenderPath<R>(cx: Scope, path: R) -> Element
-where
-    R: Routable,
-    <R as FromStr>::Err: std::fmt::Display,
-{
-    let path = path.clone();
-    render! {
-        GenericRouter::<R> {
-            config: || RouterConfig::default().history(MemoryHistory::with_initial_path(path))
-        }
-    }
-}
-
 /// An error that can occur while rendering a route or retrieving a cached route.
 #[derive(Debug, thiserror::Error)]
 pub enum IncrementalRendererError {

+ 1 - 0
packages/ssr/src/lib.rs

@@ -2,6 +2,7 @@
 
 mod cache;
 pub mod config;
+pub mod incremental;
 pub mod renderer;
 pub mod template;
 use dioxus_core::{Element, LazyNodes, Scope, VirtualDom};