mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 06:44:17 +00:00
feat: Static Site Generation (#1649)
This commit is contained in:
parent
baa5ea83fa
commit
3b864ac1a0
12 changed files with 1563 additions and 158 deletions
|
@ -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"]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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)),
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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::*;
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
447
router/src/components/static_render.rs
Normal file
447
router/src/components/static_render.rs
Normal 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 }
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue