feat: Static Site Generation (#1649)

This commit is contained in:
Village 2023-09-22 13:32:09 -04:00 committed by GitHub
parent baa5ea83fa
commit 3b864ac1a0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 1563 additions and 158 deletions

View file

@ -19,7 +19,7 @@ serde_json = "1"
parking_lot = "0.12.1"
regex = "1.7.0"
tracing = "0.1.37"
tokio = { version = "1", features = ["rt"] }
tokio = { version = "1", features = ["rt", "fs"] }
[features]
nonce = ["leptos/nonce"]

View file

@ -6,6 +6,7 @@
//! [`examples`](https://github.com/leptos-rs/leptos/tree/main/examples)
//! directory in the Leptos repository.
use actix_http::header::{HeaderName, HeaderValue};
use actix_web::{
body::BoxBody,
dev::{ServiceFactory, ServiceRequest},
@ -26,7 +27,7 @@ use leptos_meta::*;
use leptos_router::*;
use parking_lot::RwLock;
use regex::Regex;
use std::{fmt::Display, future::Future, sync::Arc};
use std::{fmt::Display, future::Future, pin::Pin, sync::Arc};
#[cfg(debug_assertions)]
use tracing::instrument;
/// This struct lets you define headers and override the status of the Response from an Element or a Server Function
@ -869,12 +870,24 @@ async fn render_app_async_helper(
/// create routes in Actix's App without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths.
pub fn generate_route_list<IV>(
app_fn: impl FnOnce() -> IV + 'static,
app_fn: impl Fn() -> IV + 'static + Clone,
) -> Vec<RouteListing>
where
IV: IntoView + 'static,
{
generate_route_list_with_exclusions(app_fn, None)
generate_route_list_with_exclusions_and_ssg(app_fn, None).0
}
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
/// create routes in Actix's App without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths.
pub fn generate_route_list_with_ssg<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
) -> (Vec<RouteListing>, StaticDataMap)
where
IV: IntoView + 'static,
{
generate_route_list_with_exclusions_and_ssg(app_fn, None)
}
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
@ -882,13 +895,28 @@ where
/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths. Adding excluded_routes
/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Actix path format
pub fn generate_route_list_with_exclusions<IV>(
app_fn: impl FnOnce() -> IV + 'static,
app_fn: impl Fn() -> IV + 'static + Clone,
excluded_routes: Option<Vec<String>>,
) -> Vec<RouteListing>
where
IV: IntoView + 'static,
{
let mut routes = leptos_router::generate_route_list_inner(app_fn);
generate_route_list_with_exclusions_and_ssg(app_fn, excluded_routes).0
}
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
/// create routes in Actix's App without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths. Adding excluded_routes
/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Actix path format
pub fn generate_route_list_with_exclusions_and_ssg<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
excluded_routes: Option<Vec<String>>,
) -> (Vec<RouteListing>, StaticDataMap)
where
IV: IntoView + 'static,
{
let (mut routes, static_data_map) =
leptos_router::generate_route_list_inner(app_fn);
// Actix's Router doesn't follow Leptos's
// Match `*` or `*someword` to replace with replace it with "/{tail.*}
@ -904,30 +932,54 @@ where
if path.is_empty() {
return RouteListing::new(
"/".to_string(),
listing.path(),
listing.mode(),
listing.methods(),
listing.static_mode(),
);
}
RouteListing::new(listing.path(), listing.mode(), listing.methods())
RouteListing::new(
listing.path(),
listing.path(),
listing.mode(),
listing.methods(),
listing.static_mode(),
)
})
.map(|listing| {
let path = wildcard_re
.replace_all(listing.path(), "{tail:.*}")
.to_string();
let path = capture_re.replace_all(&path, "{$1}").to_string();
RouteListing::new(path, listing.mode(), listing.methods())
RouteListing::new(
path,
listing.path(),
listing.mode(),
listing.methods(),
listing.static_mode(),
)
})
.collect::<Vec<_>>();
if routes.is_empty() {
vec![RouteListing::new("/", Default::default(), [Method::Get])]
} else {
// Routes to exclude from auto generation
if let Some(excluded_routes) = excluded_routes {
routes.retain(|p| !excluded_routes.iter().any(|e| e == p.path()))
}
routes
}
(
if routes.is_empty() {
vec![RouteListing::new(
"/",
"",
Default::default(),
[Method::Get],
None,
)]
} else {
// Routes to exclude from auto generation
if let Some(excluded_routes) = excluded_routes {
routes
.retain(|p| !excluded_routes.iter().any(|e| e == p.path()))
}
routes
},
static_data_map,
)
}
pub enum DataResponse<T> {
@ -935,6 +987,179 @@ pub enum DataResponse<T> {
Response(actix_web::dev::Response<BoxBody>),
}
fn handle_static_response<'a, IV>(
path: &'a str,
options: &'a LeptosOptions,
app_fn: &'a (impl Fn() -> IV + Clone + Send + 'static),
additional_context: &'a (impl Fn() + 'static + Clone + Send),
res: StaticResponse,
) -> Pin<Box<dyn Future<Output = HttpResponse<String>> + 'a>>
where
IV: IntoView + 'static,
{
Box::pin(async move {
match res {
StaticResponse::ReturnResponse {
body,
status,
content_type,
} => {
let mut res = HttpResponse::new(match status {
StaticStatusCode::Ok => StatusCode::OK,
StaticStatusCode::NotFound => StatusCode::NOT_FOUND,
StaticStatusCode::InternalServerError => {
StatusCode::INTERNAL_SERVER_ERROR
}
});
if let Some(v) = content_type {
res.headers_mut().insert(
HeaderName::from_static("content-type"),
HeaderValue::from_static(v),
);
}
res.set_body(body)
}
StaticResponse::RenderDynamic => {
handle_static_response(
path,
options,
app_fn,
additional_context,
render_dynamic(
path,
options,
app_fn.clone(),
additional_context.clone(),
)
.await,
)
.await
}
StaticResponse::RenderNotFound => {
handle_static_response(
path,
options,
app_fn,
additional_context,
not_found_page(
tokio::fs::read_to_string(not_found_path(options))
.await,
),
)
.await
}
StaticResponse::WriteFile { body, path } => {
if let Some(path) = path.parent() {
if let Err(e) = std::fs::create_dir_all(path) {
tracing::error!(
"encountered error {} writing directories {}",
e,
path.display()
);
}
}
if let Err(e) = std::fs::write(&path, &body) {
tracing::error!(
"encountered error {} writing file {}",
e,
path.display()
);
}
handle_static_response(
path.to_str().unwrap(),
options,
app_fn,
additional_context,
StaticResponse::ReturnResponse {
body,
status: StaticStatusCode::Ok,
content_type: Some("text/html"),
},
)
.await
}
}
})
}
fn static_route<IV>(
options: LeptosOptions,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + 'static + Clone + Send,
method: Method,
mode: StaticMode,
) -> Route
where
IV: IntoView + 'static,
{
match mode {
StaticMode::Incremental => {
let handler = move |req: HttpRequest| {
Box::pin({
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
async move {
handle_static_response(
req.path(),
&options,
&app_fn,
&additional_context,
incremental_static_route(
tokio::fs::read_to_string(static_file_path(
&options,
req.path(),
))
.await,
),
)
.await
}
})
};
match method {
Method::Get => web::get().to(handler),
Method::Post => web::post().to(handler),
Method::Put => web::put().to(handler),
Method::Delete => web::delete().to(handler),
Method::Patch => web::patch().to(handler),
}
}
StaticMode::Upfront => {
let handler = move |req: HttpRequest| {
Box::pin({
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
async move {
handle_static_response(
req.path(),
&options,
&app_fn,
&additional_context,
upfront_static_route(
tokio::fs::read_to_string(static_file_path(
&options,
req.path(),
))
.await,
),
)
.await
}
})
};
match method {
Method::Get => web::get().to(handler),
Method::Post => web::post().to(handler),
Method::Put => web::put().to(handler),
Method::Delete => web::delete().to(handler),
Method::Patch => web::patch().to(handler),
}
}
}
}
/// This trait allows one to pass a list of routes and a render function to Actix's router, letting us avoid
/// having to use wildcards or manually define all routes in multiple places.
pub trait LeptosRoutes {
@ -999,7 +1224,19 @@ where
let mode = listing.mode();
for method in listing.methods() {
router = router.route(
router = if let Some(static_mode) = listing.static_mode() {
router.route(
path,
static_route(
options.clone(),
app_fn.clone(),
additional_context.clone(),
method,
static_mode,
),
)
} else {
router.route(
path,
match mode {
SsrMode::OutOfOrder => {
@ -1034,7 +1271,8 @@ where
method,
),
},
);
)
};
}
}
router

View file

@ -36,7 +36,6 @@ use leptos_router::*;
use once_cell::sync::OnceCell;
use parking_lot::RwLock;
use std::{io, pin::Pin, sync::Arc, thread::available_parallelism};
use tokio::task::LocalSet;
use tokio_util::task::LocalPoolHandle;
use tracing::Instrument;
/// A struct to hold the parts of the incoming Request. Since `http::Request` isn't cloneable, we're forced
@ -1208,12 +1207,27 @@ where
/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths.
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub async fn generate_route_list<IV>(
app_fn: impl FnOnce() -> IV + 'static,
app_fn: impl Fn() -> IV + 'static + Clone,
) -> Vec<RouteListing>
where
IV: IntoView + 'static,
{
generate_route_list_with_exclusions(app_fn, None).await
generate_route_list_with_exclusions_and_ssg(app_fn, None)
.await
.0
}
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
/// create routes in Axum's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths.
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub async fn generate_route_list_with_ssg<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
) -> (Vec<RouteListing>, StaticDataMap)
where
IV: IntoView + 'static,
{
generate_route_list_with_exclusions_and_ssg(app_fn, None).await
}
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
@ -1222,34 +1236,31 @@ where
/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Axum path format
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub async fn generate_route_list_with_exclusions<IV>(
app_fn: impl FnOnce() -> IV + 'static,
app_fn: impl Fn() -> IV + 'static + Clone,
excluded_routes: Option<Vec<String>>,
) -> Vec<RouteListing>
where
IV: IntoView + 'static,
{
#[derive(Default, Clone, Debug)]
pub struct Routes(pub Arc<RwLock<Vec<RouteListing>>>);
generate_route_list_with_exclusions_and_ssg(app_fn, excluded_routes)
.await
.0
}
let routes = Routes::default();
let routes_inner = routes.clone();
let local = LocalSet::new();
// Run the local task set.
local
.run_until(async move {
tokio::task::spawn_local(async move {
let routes = leptos_router::generate_route_list_inner(app_fn);
let mut writable = routes_inner.0.write();
*writable = routes;
})
.await
.unwrap();
})
.await;
let routes = routes.0.read().to_owned();
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
/// create routes in Axum's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths. Adding excluded_routes
/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Axum path format
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub async fn generate_route_list_with_exclusions_and_ssg<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
excluded_routes: Option<Vec<String>>,
) -> (Vec<RouteListing>, StaticDataMap)
where
IV: IntoView + 'static,
{
let (routes, static_data_map) =
leptos_router::generate_route_list_inner(app_fn);
// Axum's Router defines Root routes as "/" not ""
let mut routes = routes
.into_iter()
@ -1258,8 +1269,10 @@ where
if path.is_empty() {
RouteListing::new(
"/".to_string(),
listing.path(),
listing.mode(),
listing.methods(),
listing.static_mode(),
)
} else {
listing
@ -1267,19 +1280,25 @@ where
})
.collect::<Vec<_>>();
if routes.is_empty() {
vec![RouteListing::new(
"/",
Default::default(),
[leptos_router::Method::Get],
)]
} else {
// Routes to exclude from auto generation
if let Some(excluded_routes) = excluded_routes {
routes.retain(|p| !excluded_routes.iter().any(|e| e == p.path()))
}
routes
}
(
if routes.is_empty() {
vec![RouteListing::new(
"/",
"",
Default::default(),
[leptos_router::Method::Get],
None,
)]
} else {
// Routes to exclude from auto generation
if let Some(excluded_routes) = excluded_routes {
routes
.retain(|p| !excluded_routes.iter().any(|e| e == p.path()))
}
routes
},
static_data_map,
)
}
/// This trait allows one to pass a list of routes and a render function to Axum's router, letting us avoid
@ -1317,6 +1336,208 @@ where
T: 'static;
}
fn handle_static_response<IV>(
path: String,
options: LeptosOptions,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + Clone + Send + 'static,
res: StaticResponse,
) -> Pin<Box<dyn Future<Output = Response<String>> + 'static>>
where
IV: IntoView + 'static,
{
Box::pin(async move {
match res {
StaticResponse::ReturnResponse {
body,
status,
content_type,
} => {
let mut res = Response::new(body);
if let Some(v) = content_type {
res.headers_mut().insert(
HeaderName::from_static("content-type"),
HeaderValue::from_static(v),
);
}
*res.status_mut() = match status {
StaticStatusCode::Ok => StatusCode::OK,
StaticStatusCode::NotFound => StatusCode::NOT_FOUND,
StaticStatusCode::InternalServerError => {
StatusCode::INTERNAL_SERVER_ERROR
}
};
res
}
StaticResponse::RenderDynamic => {
let res = render_dynamic(
&path,
&options,
app_fn.clone(),
additional_context.clone(),
)
.await;
handle_static_response(
path,
options,
app_fn,
additional_context,
res,
)
.await
}
StaticResponse::RenderNotFound => {
let res = not_found_page(
tokio::fs::read_to_string(not_found_path(&options)).await,
);
handle_static_response(
path,
options,
app_fn,
additional_context,
res,
)
.await
}
StaticResponse::WriteFile { body, path } => {
if let Some(path) = path.parent() {
if let Err(e) = std::fs::create_dir_all(path) {
tracing::error!(
"encountered error {} writing directories {}",
e,
path.display()
);
}
}
if let Err(e) = std::fs::write(&path, &body) {
tracing::error!(
"encountered error {} writing file {}",
e,
path.display()
);
}
handle_static_response(
path.to_str().unwrap().to_string(),
options,
app_fn,
additional_context,
StaticResponse::ReturnResponse {
body,
status: StaticStatusCode::Ok,
content_type: Some("text/html"),
},
)
.await
}
}
})
}
fn static_route<IV, S>(
router: axum::Router<S>,
path: &str,
options: LeptosOptions,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + Clone + Send + 'static,
method: leptos_router::Method,
mode: StaticMode,
) -> axum::Router<S>
where
IV: IntoView + 'static,
S: Clone + Send + Sync + 'static,
{
match mode {
StaticMode::Incremental => {
let handler = move |req: Request<Body>| {
Box::pin({
let path = req.uri().path().to_string();
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
async move {
let (tx, rx) = futures::channel::oneshot::channel();
let local_pool = get_leptos_pool();
local_pool.spawn_pinned(move || async move {
let res = incremental_static_route(
tokio::fs::read_to_string(static_file_path(
&options, &path,
))
.await,
);
let res = handle_static_response(
path.clone(),
options,
app_fn,
additional_context,
res,
)
.await;
let _ = tx.send(res);
});
rx.await.expect("to complete HTML rendering")
}
})
};
router.route(
path,
match method {
leptos_router::Method::Get => get(handler),
leptos_router::Method::Post => post(handler),
leptos_router::Method::Put => put(handler),
leptos_router::Method::Delete => delete(handler),
leptos_router::Method::Patch => patch(handler),
},
)
}
StaticMode::Upfront => {
let handler = move |req: Request<Body>| {
Box::pin({
let path = req.uri().path().to_string();
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
async move {
let (tx, rx) = futures::channel::oneshot::channel();
let local_pool = get_leptos_pool();
local_pool.spawn_pinned(move || async move {
let res = upfront_static_route(
tokio::fs::read_to_string(static_file_path(
&options, &path,
))
.await,
);
let res = handle_static_response(
path.clone(),
options,
app_fn,
additional_context,
res,
)
.await;
let _ = tx.send(res);
});
rx.await.expect("to complete HTML rendering")
}
})
};
router.route(
path,
match method {
leptos_router::Method::Get => get(handler),
leptos_router::Method::Post => post(handler),
leptos_router::Method::Put => put(handler),
leptos_router::Method::Delete => delete(handler),
leptos_router::Method::Patch => patch(handler),
},
)
}
}
}
/// The default implementation of `LeptosRoutes` which takes in a list of paths, and dispatches GET requests
/// to those paths to Leptos's renderer.
impl<S> LeptosRoutes<S> for axum::Router<S>
@ -1353,7 +1574,18 @@ where
let path = listing.path();
for method in listing.methods() {
router = router.route(
router = if let Some(static_mode) = listing.static_mode() {
static_route(
router,
path,
LeptosOptions::from_ref(options),
app_fn.clone(),
additional_context.clone(),
method,
static_mode,
)
} else {
router.route(
path,
match listing.mode() {
SsrMode::OutOfOrder => {
@ -1414,7 +1646,8 @@ where
}
}
},
);
)
};
}
}
router

View file

@ -23,7 +23,7 @@ use leptos_meta::{generate_head_metadata_separated, MetaContext};
use leptos_router::*;
use parking_lot::RwLock;
use std::{pin::Pin, sync::Arc};
use tokio::task::{spawn_blocking, LocalSet};
use tokio::task::spawn_blocking;
use viz::{
headers::{HeaderMap, HeaderName, HeaderValue},
Body, Bytes, Error, Handler, IntoResponse, Request, RequestExt, Response,
@ -989,46 +989,54 @@ where
/// create routes in Viz's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generate Viz compatible paths.
pub async fn generate_route_list<IV>(
app_fn: impl FnOnce() -> IV + 'static,
app_fn: impl Fn() -> IV + 'static + Clone,
) -> Vec<RouteListing>
where
IV: IntoView + 'static,
{
generate_route_list_with_exclusions(app_fn, None).await
generate_route_list_with_exclusions_and_ssg(app_fn, None)
.await
.0
}
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
/// create routes in Viz's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generate Viz compatible paths.
pub async fn generate_route_list_with_ssg<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
) -> (Vec<RouteListing>, StaticDataMap)
where
IV: IntoView + 'static,
{
generate_route_list_with_exclusions_and_ssg(app_fn, None).await
}
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
/// create routes in Viz's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generate Viz compatible paths.
pub async fn generate_route_list_with_exclusions<IV>(
app_fn: impl FnOnce() -> IV + 'static,
app_fn: impl Fn() -> IV + 'static + Clone,
excluded_routes: Option<Vec<String>>,
) -> Vec<RouteListing>
where
IV: IntoView + 'static,
{
#[derive(Default, Clone, Debug)]
pub struct Routes(pub Arc<RwLock<Vec<RouteListing>>>);
let routes = Routes::default();
let routes_inner = routes.clone();
let local = LocalSet::new();
// Run the local task set.
local
.run_until(async move {
tokio::task::spawn_local(async move {
let routes = leptos_router::generate_route_list_inner(app_fn);
let mut writable = routes_inner.0.write();
*writable = routes;
})
.await
.unwrap();
})
.await;
let routes = routes.0.read().to_owned();
generate_route_list_with_exclusions_and_ssg(app_fn, excluded_routes)
.await
.0
}
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
/// create routes in Viz's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generate Viz compatible paths.
pub async fn generate_route_list_with_exclusions_and_ssg<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
excluded_routes: Option<Vec<String>>,
) -> (Vec<RouteListing>, StaticDataMap)
where
IV: IntoView + 'static,
{
let (routes, static_data_map) =
leptos_router::generate_route_list_inner(app_fn);
// Viz's Router defines Root routes as "/" not ""
let mut routes = routes
.into_iter()
@ -1037,8 +1045,10 @@ where
if path.is_empty() {
RouteListing::new(
"/".to_string(),
listing.path(),
listing.mode(),
listing.methods(),
listing.static_mode(),
)
} else {
listing
@ -1046,17 +1056,260 @@ where
})
.collect::<Vec<_>>();
if routes.is_empty() {
vec![RouteListing::new(
"/",
Default::default(),
[leptos_router::Method::Get],
)]
} else {
if let Some(excluded_routes) = excluded_routes {
routes.retain(|p| !excluded_routes.iter().any(|e| e == p.path()))
(
if routes.is_empty() {
vec![RouteListing::new(
"/",
"",
Default::default(),
[leptos_router::Method::Get],
None,
)]
} else {
if let Some(excluded_routes) = excluded_routes {
routes
.retain(|p| !excluded_routes.iter().any(|e| e == p.path()))
}
routes
},
static_data_map,
)
}
fn handle_static_response<IV>(
path: String,
options: LeptosOptions,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
additional_context: impl Fn() + Clone + Send + Sync + 'static,
res: StaticResponse,
) -> Pin<Box<dyn Future<Output = Result<Response>> + 'static>>
where
IV: IntoView + 'static,
{
Box::pin(async move {
match res {
StaticResponse::ReturnResponse {
body,
status,
content_type,
} => {
let mut res = Response::html(body);
if let Some(v) = content_type {
res.headers_mut().insert(
HeaderName::from_static("content-type"),
HeaderValue::from_static(v),
);
}
*res.status_mut() = match status {
StaticStatusCode::Ok => StatusCode::OK,
StaticStatusCode::NotFound => StatusCode::NOT_FOUND,
StaticStatusCode::InternalServerError => {
StatusCode::INTERNAL_SERVER_ERROR
}
};
Ok(res)
}
StaticResponse::RenderDynamic => {
let res = render_dynamic(
&path,
&options,
app_fn.clone(),
additional_context.clone(),
)
.await;
handle_static_response(
path,
options,
app_fn,
additional_context,
res,
)
.await
}
StaticResponse::RenderNotFound => {
let res = not_found_page(
tokio::fs::read_to_string(not_found_path(&options)).await,
);
handle_static_response(
path,
options,
app_fn,
additional_context,
res,
)
.await
}
StaticResponse::WriteFile { body, path } => {
if let Some(path) = path.parent() {
if let Err(e) = std::fs::create_dir_all(path) {
tracing::error!(
"encountered error {} writing directories {}",
e,
path.display()
);
}
}
if let Err(e) = std::fs::write(&path, &body) {
tracing::error!(
"encountered error {} writing file {}",
e,
path.display()
);
}
handle_static_response(
path.to_str().unwrap().to_string(),
options,
app_fn,
additional_context,
StaticResponse::ReturnResponse {
body,
status: StaticStatusCode::Ok,
content_type: Some("text/html"),
},
)
.await
}
}
})
}
fn static_route<IV>(
router: Router,
path: &str,
options: LeptosOptions,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
additional_context: impl Fn() + Clone + Send + Sync + 'static,
method: leptos_router::Method,
mode: StaticMode,
) -> Router
where
IV: IntoView + 'static,
{
match mode {
StaticMode::Incremental => {
let handler = move |req: Request| {
Box::pin({
let path = req.path().to_string();
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
async move {
let (tx, rx) = futures::channel::oneshot::channel();
spawn_blocking(move || {
let path = path.clone();
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
tokio::runtime::Runtime::new()
.expect("couldn't spawn runtime")
.block_on({
let path = path.clone();
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context =
additional_context.clone();
async move {
tokio::task::LocalSet::new().run_until(async {
let res = incremental_static_route(
tokio::fs::read_to_string(
static_file_path(
&options,
&path,
),
)
.await,
);
let res = handle_static_response(
path.clone(),
options,
app_fn,
additional_context,
res,
)
.await;
let _ = tx.send(res);
}).await;
}
})
});
rx.await.expect("to complete HTML rendering")
}
})
};
match method {
leptos_router::Method::Get => router.get(path, handler),
leptos_router::Method::Post => router.post(path, handler),
leptos_router::Method::Put => router.put(path, handler),
leptos_router::Method::Delete => router.delete(path, handler),
leptos_router::Method::Patch => router.patch(path, handler),
}
}
StaticMode::Upfront => {
let handler = move |req: Request| {
Box::pin({
let path = req.path().to_string();
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
async move {
let (tx, rx) = futures::channel::oneshot::channel();
spawn_blocking(move || {
let path = path.clone();
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
tokio::runtime::Runtime::new()
.expect("couldn't spawn runtime")
.block_on({
let path = path.clone();
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context =
additional_context.clone();
async move {
tokio::task::LocalSet::new()
.run_until(async {
let res = upfront_static_route(
tokio::fs::read_to_string(
static_file_path(
&options, &path,
),
)
.await,
);
let res =
handle_static_response(
path.clone(),
options,
app_fn,
additional_context,
res,
)
.await;
let _ = tx.send(res);
})
.await;
}
})
});
rx.await.expect("to complete HTML rendering")
}
})
};
match method {
leptos_router::Method::Get => router.get(path, handler),
leptos_router::Method::Post => router.post(path, handler),
leptos_router::Method::Put => router.put(path, handler),
leptos_router::Method::Delete => router.delete(path, handler),
leptos_router::Method::Patch => router.patch(path, handler),
}
}
routes
}
}
@ -1120,63 +1373,117 @@ impl LeptosRoutes for Router {
let path = listing.path();
let mode = listing.mode();
listing.methods().fold(router, |router, method| match mode {
SsrMode::OutOfOrder => {
let s = render_app_to_stream_with_context(
listing.methods().fold(router, |router, method| {
if let Some(static_mode) = listing.static_mode() {
static_route(
router,
path,
options.clone(),
additional_context.clone(),
app_fn.clone(),
);
match method {
leptos_router::Method::Get => router.get(path, s),
leptos_router::Method::Post => router.post(path, s),
leptos_router::Method::Put => router.put(path, s),
leptos_router::Method::Delete => router.delete(path, s),
leptos_router::Method::Patch => router.patch(path, s),
}
}
SsrMode::PartiallyBlocked => {
let s =
additional_context.clone(),
method,
static_mode,
)
} else {
match mode {
SsrMode::OutOfOrder => {
let s = render_app_to_stream_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
);
match method {
leptos_router::Method::Get => {
router.get(path, s)
}
leptos_router::Method::Post => {
router.post(path, s)
}
leptos_router::Method::Put => {
router.put(path, s)
}
leptos_router::Method::Delete => {
router.delete(path, s)
}
leptos_router::Method::Patch => {
router.patch(path, s)
}
}
}
SsrMode::PartiallyBlocked => {
let s =
render_app_to_stream_with_context_and_replace_blocks(
options.clone(),
additional_context.clone(),
app_fn.clone(),
true,
);
match method {
leptos_router::Method::Get => router.get(path, s),
leptos_router::Method::Post => router.post(path, s),
leptos_router::Method::Put => router.put(path, s),
leptos_router::Method::Delete => router.delete(path, s),
leptos_router::Method::Patch => router.patch(path, s),
}
}
SsrMode::InOrder => {
let s = render_app_to_stream_in_order_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
);
match method {
leptos_router::Method::Get => router.get(path, s),
leptos_router::Method::Post => router.post(path, s),
leptos_router::Method::Put => router.put(path, s),
leptos_router::Method::Delete => router.delete(path, s),
leptos_router::Method::Patch => router.patch(path, s),
}
}
SsrMode::Async => {
let s = render_app_async_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
);
match method {
leptos_router::Method::Get => router.get(path, s),
leptos_router::Method::Post => router.post(path, s),
leptos_router::Method::Put => router.put(path, s),
leptos_router::Method::Delete => router.delete(path, s),
leptos_router::Method::Patch => router.patch(path, s),
match method {
leptos_router::Method::Get => {
router.get(path, s)
}
leptos_router::Method::Post => {
router.post(path, s)
}
leptos_router::Method::Put => {
router.put(path, s)
}
leptos_router::Method::Delete => {
router.delete(path, s)
}
leptos_router::Method::Patch => {
router.patch(path, s)
}
}
}
SsrMode::InOrder => {
let s = render_app_to_stream_in_order_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
);
match method {
leptos_router::Method::Get => {
router.get(path, s)
}
leptos_router::Method::Post => {
router.post(path, s)
}
leptos_router::Method::Put => {
router.put(path, s)
}
leptos_router::Method::Delete => {
router.delete(path, s)
}
leptos_router::Method::Patch => {
router.patch(path, s)
}
}
}
SsrMode::Async => {
let s = render_app_async_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
);
match method {
leptos_router::Method::Get => {
router.get(path, s)
}
leptos_router::Method::Post => {
router.post(path, s)
}
leptos_router::Method::Put => {
router.put(path, s)
}
leptos_router::Method::Delete => {
router.delete(path, s)
}
leptos_router::Method::Patch => {
router.patch(path, s)
}
}
}
}
}
})

View file

@ -66,6 +66,10 @@ pub struct LeptosOptions {
#[builder(default)]
#[serde(default)]
pub reload_ws_protocol: ReloadWSProtocol,
/// The path of a custom 404 Not Found page to display when statically serving content, defaults to `site_root/404.html`
#[builder(default = default_not_found_path())]
#[serde(default = "default_not_found_path")]
pub not_found_path: String,
}
impl LeptosOptions {
@ -103,6 +107,7 @@ impl LeptosOptions {
reload_ws_protocol: ws_from_str(
env_w_default("LEPTOS_RELOAD_WS_PROTOCOL", "ws")?.as_str(),
)?,
not_found_path: env_w_default("LEPTOS_NOT_FOUND_PATH", "/404")?,
})
}
}
@ -126,6 +131,11 @@ fn default_site_addr() -> SocketAddr {
fn default_reload_port() -> u32 {
3001
}
fn default_not_found_path() -> String {
"/404".to_string()
}
fn env_wo_default(key: &str) -> Result<Option<String>, LeptosConfigError> {
match std::env::var(key) {
Ok(val) => Ok(Some(val)),

View file

@ -10,6 +10,8 @@ description = "Router for the Leptos web framework."
[dependencies]
leptos = { workspace = true }
leptos_integration_utils = { workspace = true, optional = true }
leptos_meta = { workspace = true, optional = true }
cached = { version = "0.45.0", optional = true }
cfg-if = "1"
common_macros = "0.1"
@ -59,7 +61,15 @@ features = [
default = []
csr = ["leptos/csr"]
hydrate = ["leptos/hydrate"]
ssr = ["leptos/ssr", "dep:cached", "dep:lru", "dep:url", "dep:regex"]
ssr = [
"leptos/ssr",
"dep:cached",
"dep:lru",
"dep:url",
"dep:regex",
"dep:leptos_integration_utils",
"dep:leptos_meta",
]
nightly = ["leptos/nightly"]
[package.metadata.cargo-all-features]

View file

@ -6,6 +6,7 @@ mod redirect;
mod route;
mod router;
mod routes;
mod static_render;
pub use form::*;
pub use link::*;
@ -15,3 +16,4 @@ pub use redirect::*;
pub use route::*;
pub use router::*;
pub use routes::*;
pub use static_render::*;

View file

@ -1,12 +1,14 @@
use crate::{
matching::{resolve_path, PathMatch, RouteDefinition, RouteMatch},
ParamsMap, RouterContext, SsrMode,
ParamsMap, RouterContext, SsrMode, StaticData, StaticMode, StaticParamsMap,
};
use leptos::{leptos_dom::Transparent, *};
use std::{
any::Any,
borrow::Cow,
cell::{Cell, RefCell},
future::Future,
pin::Pin,
rc::Rc,
};
@ -78,6 +80,8 @@ where
ssr,
methods,
data,
None,
None,
)
}
@ -136,12 +140,64 @@ where
ssr,
methods,
data,
None,
None,
)
}
/// Describes a portion of the nested layout of the app, specifying the route it should match,
/// the element it should display, and data that should be loaded alongside the route.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "info", skip_all,)
)]
#[component(transparent)]
pub fn StaticRoute<E, F, P, S>(
/// The path fragment that this route should match. This can be static (`users`),
/// include a parameter (`:id`) or an optional parameter (`:id?`), or match a
/// wildcard (`user/*any`).
path: P,
/// The view that should be shown when this route is matched. This can be any function
/// that returns a type that implements [IntoView] (like `|| view! { <p>"Show this"</p> })`
/// or `|| view! { <MyComponent/>` } or even, for a component with no props, `MyComponent`).
view: F,
/// Creates a map of the params that should be built for a particular route.
#[prop(optional)]
static_params: Option<S>,
/// The static route mode
#[prop(optional)]
mode: StaticMode,
/// A data-loading function that will be called when the route is matched. Its results can be
/// accessed with [`use_route_data`](crate::use_route_data).
#[prop(optional, into)]
data: Option<Loader>,
/// `children` may be empty or include nested routes.
#[prop(optional)]
children: Option<Children>,
) -> impl IntoView
where
E: IntoView,
F: Fn() -> E + 'static,
P: std::fmt::Display,
S: Fn() -> Pin<Box<dyn Future<Output = StaticParamsMap>>> + 'static,
{
define_route(
children,
path.to_string(),
Rc::new(move || view().into_view()),
SsrMode::default(),
&[Method::Get],
data,
Some(mode),
static_params.map(|s| Rc::new(s) as _),
)
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "info", skip_all,)
)]
#[allow(clippy::too_many_arguments)]
pub(crate) fn define_route(
children: Option<Children>,
path: String,
@ -149,6 +205,8 @@ pub(crate) fn define_route(
ssr_mode: SsrMode,
methods: &'static [Method],
data: Option<Loader>,
static_mode: Option<StaticMode>,
static_params: Option<StaticData>,
) -> RouteDefinition {
let children = children
.map(|children| {
@ -179,6 +237,8 @@ pub(crate) fn define_route(
ssr_mode,
methods,
data,
static_mode,
static_params,
}
}

View file

@ -313,6 +313,8 @@ impl Branches {
base,
&mut Vec::new(),
&mut branches,
true,
base,
);
current.insert(base.to_string(), branches);
}
@ -570,9 +572,16 @@ fn create_branches(
base: &str,
stack: &mut Vec<RouteData>,
branches: &mut Vec<Branch>,
static_valid: bool,
parents_path: &str,
) {
for def in route_defs {
let routes = create_routes(def, base);
let routes = create_routes(
def,
base,
static_valid && def.static_mode.is_some(),
parents_path,
);
for route in routes {
stack.push(route.clone());
@ -580,7 +589,14 @@ fn create_branches(
let branch = create_branch(stack, branches.len());
branches.push(branch);
} else {
create_branches(&def.children, &route.pattern, stack, branches);
create_branches(
&def.children,
&route.pattern,
stack,
branches,
static_valid && route.key.static_mode.is_some(),
&format!("{}{}", parents_path, def.path),
);
}
stack.pop();
@ -603,9 +619,21 @@ pub(crate) fn create_branch(routes: &[RouteData], index: usize) -> Branch {
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "info", skip_all,)
)]
fn create_routes(route_def: &RouteDefinition, base: &str) -> Vec<RouteData> {
fn create_routes(
route_def: &RouteDefinition,
base: &str,
static_valid: bool,
parents_path: &str,
) -> Vec<RouteData> {
let RouteDefinition { children, .. } = route_def;
let is_leaf = children.is_empty();
if is_leaf && route_def.static_mode.is_some() && !static_valid {
panic!(
"Static rendering is not valid for route '{}{}', all parent \
routes must also be statically renderable.",
parents_path, route_def.path
);
}
let mut acc = Vec::new();
for original_path in expand_optionals(&route_def.path) {
let path = join_paths(base, &original_path);

View file

@ -0,0 +1,447 @@
#[cfg(feature = "ssr")]
use crate::{RouteListing, RouterIntegrationContext, ServerIntegration};
#[cfg(feature = "ssr")]
use leptos::{provide_context, IntoView, LeptosOptions};
#[cfg(feature = "ssr")]
use leptos_meta::MetaContext;
use linear_map::LinearMap;
use serde::{Deserialize, Serialize};
#[cfg(feature = "ssr")]
use std::path::Path;
use std::{
collections::HashMap,
fmt::Display,
future::Future,
hash::{Hash, Hasher},
path::PathBuf,
pin::Pin,
rc::Rc,
};
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct StaticParamsMap(pub LinearMap<String, Vec<String>>);
impl StaticParamsMap {
/// Create a new empty `StaticParamsMap`.
#[inline]
pub fn new() -> Self {
Self::default()
}
/// Insert a value into the map.
#[inline]
pub fn insert(&mut self, key: impl ToString, value: Vec<String>) {
self.0.insert(key.to_string(), value);
}
/// Get a value from the map.
#[inline]
pub fn get(&self, key: &str) -> Option<&Vec<String>> {
self.0.get(key)
}
}
#[doc(hidden)]
#[derive(Debug)]
pub struct StaticPath<'b, 'a: 'b> {
path: &'a str,
segments: Vec<StaticPathSegment<'a>>,
params: LinearMap<&'a str, &'b Vec<String>>,
}
#[doc(hidden)]
#[derive(Debug)]
enum StaticPathSegment<'a> {
Static(&'a str),
Param(&'a str),
Wildcard(&'a str),
}
impl<'b, 'a: 'b> StaticPath<'b, 'a> {
pub fn new(path: &'a str) -> StaticPath<'b, 'a> {
use StaticPathSegment::*;
Self {
path,
segments: path
.split('/')
.filter(|s| !s.is_empty())
.map(|s| match s.chars().next() {
Some(':') => Param(&s[1..]),
Some('*') => Wildcard(&s[1..]),
_ => Static(s),
})
.collect::<Vec<_>>(),
params: LinearMap::new(),
}
}
pub fn add_params(&mut self, params: &'b StaticParamsMap) {
use StaticPathSegment::*;
for segment in self.segments.iter() {
match segment {
Param(name) | Wildcard(name) => {
if let Some(value) = params.get(name) {
self.params.insert(name, value);
}
}
_ => {}
}
}
}
pub fn into_paths(self) -> Vec<ResolvedStaticPath> {
use StaticPathSegment::*;
let mut paths = vec![ResolvedStaticPath(String::new())];
for segment in self.segments {
match segment {
Static(s) => {
paths = paths
.into_iter()
.map(|p| ResolvedStaticPath(format!("{}/{}", p, s)))
.collect::<Vec<_>>();
}
Param(name) | Wildcard(name) => {
let mut new_paths = vec![];
for path in paths {
let Some(params) = self.params.get(name) else {
panic!(
"missing param {} for path: {}",
name, self.path
);
};
for val in params.iter() {
new_paths.push(ResolvedStaticPath(format!(
"{}/{}",
path, val
)));
}
}
paths = new_paths;
}
}
}
paths
}
pub fn parent(&self) -> Option<StaticPath<'b, 'a>> {
if self.path == "/" || self.path.is_empty() {
return None;
}
self.path
.rfind('/')
.map(|i| StaticPath::new(&self.path[..i]))
}
pub fn parents(&self) -> Vec<StaticPath<'b, 'a>> {
let mut parents = vec![];
let mut parent = self.parent();
while let Some(p) = parent {
parent = p.parent();
parents.push(p);
}
parents
}
pub fn path(&self) -> &'a str {
self.path
}
}
impl Hash for StaticPath<'_, '_> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.path.hash(state);
}
}
impl StaticPath<'_, '_> {}
#[doc(hidden)]
#[repr(transparent)]
pub struct ResolvedStaticPath(pub String);
impl Display for ResolvedStaticPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl ResolvedStaticPath {
#[cfg(feature = "ssr")]
pub async fn build<IV>(
&self,
options: &LeptosOptions,
app_fn: impl Fn() -> IV + 'static + Clone,
additional_context: impl Fn() + 'static + Clone,
) -> String
where
IV: IntoView + 'static,
{
let url = format!("http://leptos{}", self);
let app = {
let app_fn = app_fn.clone();
move || {
provide_context(RouterIntegrationContext::new(
ServerIntegration { path: url },
));
provide_context(MetaContext::new());
(app_fn)().into_view()
}
};
let (stream, runtime) = leptos::ssr::render_to_stream_in_order_with_prefix_undisposed_with_context(app, move || "".into(), additional_context.clone());
leptos_integration_utils::build_async_response(stream, options, runtime)
.await
}
#[cfg(feature = "ssr")]
pub async fn write<IV>(
&self,
options: &LeptosOptions,
app_fn: impl Fn() -> IV + 'static + Clone,
additional_context: impl Fn() + 'static + Clone,
) -> Result<String, std::io::Error>
where
IV: IntoView + 'static,
{
let html = self.build(options, app_fn, additional_context).await;
let path = Path::new(&options.site_root)
.join(format!("{}.static.html", self.0.trim_start_matches('/')));
if let Some(path) = path.parent() {
std::fs::create_dir_all(path)?
}
std::fs::write(path, &html)?;
Ok(html)
}
}
#[cfg(feature = "ssr")]
pub async fn build_static_routes<IV>(
options: &LeptosOptions,
app_fn: impl Fn() -> IV + 'static + Clone,
routes: &[RouteListing],
static_data_map: &StaticDataMap,
) -> Result<(), std::io::Error>
where
IV: IntoView + 'static,
{
build_static_routes_with_additional_context(
options,
app_fn,
|| {},
routes,
static_data_map,
)
.await
}
#[cfg(feature = "ssr")]
pub async fn build_static_routes_with_additional_context<IV>(
options: &LeptosOptions,
app_fn: impl Fn() -> IV + 'static + Clone,
additional_context: impl Fn() + 'static + Clone,
routes: &[RouteListing],
static_data_map: &StaticDataMap,
) -> Result<(), std::io::Error>
where
IV: IntoView + 'static,
{
let mut static_data: HashMap<&str, StaticParamsMap> = HashMap::new();
for (key, value) in static_data_map {
match value {
Some(value) => static_data.insert(key, value.as_ref()().await),
None => static_data.insert(key, StaticParamsMap::default()),
};
}
let static_routes = routes
.iter()
.filter(|route| route.static_mode().is_some())
.collect::<Vec<_>>();
// TODO: maybe make this concurrent in some capacity
for route in static_routes {
let mut path = StaticPath::new(route.leptos_path());
for p in path.parents().into_iter().rev() {
if let Some(data) = static_data.get(p.path()) {
path.add_params(data);
}
}
if let Some(data) = static_data.get(path.path()) {
path.add_params(data);
}
for path in path.into_paths() {
println!("building static route: {}", path);
path.write(options, app_fn.clone(), additional_context.clone())
.await?;
}
}
Ok(())
}
#[doc(hidden)]
#[cfg(feature = "ssr")]
pub fn purge_dir_of_static_files(path: PathBuf) -> Result<(), std::io::Error> {
for entry in path.read_dir()? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
purge_dir_of_static_files(path)?;
} else if path.is_file() {
if let Some(name) = path.file_name().and_then(|i| i.to_str()) {
if name.ends_with(".static.html") {
std::fs::remove_file(path)?;
}
}
}
}
Ok(())
}
/// Purge all statically generated route files
#[cfg(feature = "ssr")]
pub fn purge_all_static_routes<IV>(
options: &LeptosOptions,
) -> Result<(), std::io::Error> {
purge_dir_of_static_files(Path::new(&options.site_root).to_path_buf())
}
pub type StaticData = Rc<StaticDataFn>;
pub type StaticDataFn =
dyn Fn() -> Pin<Box<dyn Future<Output = StaticParamsMap>>> + 'static;
pub type StaticDataMap = HashMap<String, Option<StaticData>>;
/// The mode to use when rendering the route statically.
/// On mode `Upfront`, the route will be built with the server is started using the provided static
/// data. On mode `Incremental`, the route will be built on the first request to it and then cached
/// and returned statically for subsequent requests.
#[derive(Default, Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum StaticMode {
#[default]
Upfront,
Incremental,
}
#[doc(hidden)]
pub enum StaticStatusCode {
Ok,
NotFound,
InternalServerError,
}
#[doc(hidden)]
pub enum StaticResponse {
ReturnResponse {
body: String,
status: StaticStatusCode,
content_type: Option<&'static str>,
},
RenderDynamic,
RenderNotFound,
WriteFile {
body: String,
path: PathBuf,
},
}
#[doc(hidden)]
#[inline(always)]
#[cfg(feature = "ssr")]
pub fn static_file_path(options: &LeptosOptions, path: &str) -> String {
format!("{}{}.static.html", options.site_root, path)
}
#[doc(hidden)]
#[inline(always)]
#[cfg(feature = "ssr")]
pub fn not_found_path(options: &LeptosOptions) -> String {
format!(
"{}{}.static.html",
options.site_root, options.not_found_path
)
}
#[doc(hidden)]
#[inline(always)]
pub fn upfront_static_route(
res: Result<String, std::io::Error>,
) -> StaticResponse {
match res {
Ok(body) => StaticResponse::ReturnResponse {
body,
status: StaticStatusCode::Ok,
content_type: Some("text/html"),
},
Err(e) => match e.kind() {
std::io::ErrorKind::NotFound => StaticResponse::RenderNotFound,
_ => {
tracing::error!("error reading file: {}", e);
StaticResponse::ReturnResponse {
body: "Internal Server Error".into(),
status: StaticStatusCode::InternalServerError,
content_type: None,
}
}
},
}
}
#[doc(hidden)]
#[inline(always)]
pub fn not_found_page(res: Result<String, std::io::Error>) -> StaticResponse {
match res {
Ok(body) => StaticResponse::ReturnResponse {
body,
status: StaticStatusCode::NotFound,
content_type: Some("text/html"),
},
Err(e) => match e.kind() {
std::io::ErrorKind::NotFound => StaticResponse::ReturnResponse {
body: "Not Found".into(),
status: StaticStatusCode::Ok,
content_type: None,
},
_ => {
tracing::error!("error reading not found file: {}", e);
StaticResponse::ReturnResponse {
body: "Internal Server Error".into(),
status: StaticStatusCode::InternalServerError,
content_type: None,
}
}
},
}
}
#[doc(hidden)]
pub fn incremental_static_route(
res: Result<String, std::io::Error>,
) -> StaticResponse {
match res {
Ok(body) => StaticResponse::ReturnResponse {
body,
status: StaticStatusCode::Ok,
content_type: Some("text/html"),
},
Err(_) => StaticResponse::RenderDynamic,
}
}
#[doc(hidden)]
#[cfg(feature = "ssr")]
pub async fn render_dynamic<IV>(
path: &str,
options: &LeptosOptions,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + 'static + Clone + Send,
) -> StaticResponse
where
IV: IntoView + 'static,
{
let body = ResolvedStaticPath(path.into())
.build(options, app_fn, additional_context)
.await;
let path = Path::new(&options.site_root)
.join(format!("{}.static.html", path.trim_start_matches('/')));
StaticResponse::WriteFile { body, path }
}

View file

@ -1,8 +1,13 @@
use crate::{
Branch, Method, RouterIntegrationContext, ServerIntegration, SsrMode,
StaticDataMap, StaticMode, StaticParamsMap, StaticPath,
};
use leptos::*;
use std::{cell::RefCell, collections::HashSet, rc::Rc};
use std::{
cell::RefCell,
collections::{HashMap, HashSet},
rc::Rc,
};
/// Context to contain all possible routes.
#[derive(Clone, Default, Debug)]
@ -12,21 +17,27 @@ pub struct PossibleBranchContext(pub(crate) Rc<RefCell<Vec<Branch>>>);
/// A route that this application can serve.
pub struct RouteListing {
path: String,
leptos_path: String,
mode: SsrMode,
methods: HashSet<Method>,
static_mode: Option<StaticMode>,
}
impl RouteListing {
/// Create a route listing from its parts.
pub fn new(
path: impl ToString,
leptos_path: impl ToString,
mode: SsrMode,
methods: impl IntoIterator<Item = Method>,
static_mode: Option<StaticMode>,
) -> Self {
Self {
path: path.to_string(),
leptos_path: leptos_path.to_string(),
mode,
methods: methods.into_iter().collect(),
static_mode,
}
}
@ -35,6 +46,11 @@ impl RouteListing {
&self.path
}
/// The leptos-formatted path this route handles.
pub fn leptos_path(&self) -> &str {
&self.leptos_path
}
/// The rendering mode for this path.
pub fn mode(&self) -> SsrMode {
self.mode
@ -44,6 +60,43 @@ impl RouteListing {
pub fn methods(&self) -> impl Iterator<Item = Method> + '_ {
self.methods.iter().copied()
}
/// Whether this route is statically rendered.
#[inline(always)]
pub fn static_mode(&self) -> Option<StaticMode> {
self.static_mode
}
/// Build a route statically, will return `Ok(true)` on success or `Ok(false)` when the route
/// is not marked as statically rendered. All route parameters to use when resolving all paths
/// to render should be passed in the `params` argument.
pub async fn build_static<IV>(
&self,
options: &LeptosOptions,
app_fn: impl Fn() -> IV + Send + 'static + Clone,
additional_context: impl Fn() + Send + 'static + Clone,
params: &StaticParamsMap,
) -> Result<bool, std::io::Error>
where
IV: IntoView + 'static,
{
match self.static_mode {
None => Ok(false),
Some(_) => {
let mut path = StaticPath::new(&self.leptos_path);
path.add_params(params);
for path in path.into_paths() {
path.write(
options,
app_fn.clone(),
additional_context.clone(),
)
.await?;
}
Ok(true)
}
}
}
}
/// Generates a list of all routes this application could possibly serve. This returns the raw routes in the leptos_router
@ -54,8 +107,8 @@ impl RouteListing {
/// [`axum`]: <https://docs.rs/axum/>
/// [`viz`]: <https://docs.rs/viz/>
pub fn generate_route_list_inner<IV>(
app_fn: impl FnOnce() -> IV + 'static,
) -> Vec<RouteListing>
app_fn: impl Fn() -> IV + 'static + Clone,
) -> (Vec<RouteListing>, StaticDataMap)
where
IV: IntoView + 'static,
{
@ -74,6 +127,7 @@ where
leptos::suppress_resource_load(false);
let branches = branches.0.borrow();
let mut static_data_map: StaticDataMap = HashMap::new();
let routes = branches
.iter()
.flat_map(|branch| {
@ -89,15 +143,26 @@ where
.flat_map(|route| route.key.methods)
.copied()
.collect::<HashSet<_>>();
let pattern =
branch.routes.last().map(|route| route.pattern.clone());
pattern.map(|path| RouteListing {
let route = branch
.routes
.last()
.map(|route| (route.key.static_mode, route.pattern.clone()));
for route in branch.routes.iter() {
static_data_map.insert(
route.pattern.to_string(),
route.key.static_params.clone(),
);
}
route.map(|(static_mode, path)| RouteListing {
leptos_path: path.clone(),
path,
mode,
methods: methods.clone(),
static_mode,
})
})
.collect();
.collect::<Vec<_>>();
runtime.dispose();
routes
(routes, static_data_map)
}

View file

@ -1,4 +1,4 @@
use crate::{Loader, Method, SsrMode};
use crate::{Loader, Method, SsrMode, StaticData, StaticMode};
use leptos::leptos_dom::View;
use std::rc::Rc;
@ -21,6 +21,10 @@ pub struct RouteDefinition {
pub methods: &'static [Method],
/// A data loader function that will be called when this route is matched.
pub(crate) data: Option<Loader>,
/// The route's preferred mode of static generation, if any
pub static_mode: Option<StaticMode>,
/// The data required to fill any dynamic segments in the path during static rendering.
pub static_params: Option<StaticData>,
}
impl std::fmt::Debug for RouteDefinition {
@ -29,6 +33,7 @@ impl std::fmt::Debug for RouteDefinition {
.field("path", &self.path)
.field("children", &self.children)
.field("ssr_mode", &self.ssr_mode)
.field("static_render", &self.static_mode)
.finish()
}
}