mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 06:44:17 +00:00
feat: add support for static routing and incremental static regeneration (#2875)
This commit is contained in:
parent
9fc26e609c
commit
e7bb859cd9
37 changed files with 1916 additions and 606 deletions
13
examples/static_routing/.gitignore
vendored
Normal file
13
examples/static_routing/.gitignore
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
pkg
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# node e2e test tools and outputs
|
||||
node_modules/
|
||||
test-results/
|
||||
end2end/playwright-report/
|
||||
playwright/.cache/
|
115
examples/static_routing/Cargo.toml
Normal file
115
examples/static_routing/Cargo.toml
Normal file
|
@ -0,0 +1,115 @@
|
|||
[package]
|
||||
name = "static_routing"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
console_error_panic_hook = "0.1.7"
|
||||
console_log = "1.0"
|
||||
leptos = { path = "../../leptos", features = [
|
||||
"hydration",
|
||||
] } #"nightly", "hydration"] }
|
||||
leptos_meta = { path = "../../meta" }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_router = { path = "../../router" }
|
||||
log = "0.4.22"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
thiserror = "1.0"
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = [
|
||||
"fs",
|
||||
"rt-multi-thread",
|
||||
"macros",
|
||||
], optional = true }
|
||||
tokio-stream = { version = "0.1", features = ["fs"], optional = true }
|
||||
futures = "0.3"
|
||||
wasm-bindgen = "0.2.93"
|
||||
notify = { version = "6", optional = true }
|
||||
http = { version = "1", optional = true }
|
||||
|
||||
[features]
|
||||
hydrate = ["leptos/hydrate"]
|
||||
ssr = [
|
||||
"dep:axum",
|
||||
"dep:tower",
|
||||
"dep:tower-http",
|
||||
"dep:tokio",
|
||||
"dep:tokio-stream",
|
||||
"leptos/ssr",
|
||||
"leptos_meta/ssr",
|
||||
"dep:leptos_axum",
|
||||
"leptos_router/ssr",
|
||||
"dep:notify",
|
||||
"dep:http"
|
||||
]
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
|
||||
[profile.wasm-release]
|
||||
inherits = "release"
|
||||
opt-level = 'z'
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["axum", "tower", "tower-http", "tokio", "sqlx", "leptos_axum"]
|
||||
skip_feature_sets = [["ssr", "hydrate"]]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
output-name = "ssr_modes"
|
||||
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
|
||||
site-root = "target/site"
|
||||
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
|
||||
# Defaults to pkg
|
||||
site-pkg-dir = "pkg"
|
||||
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
|
||||
style-file = "style/main.scss"
|
||||
# Assets source dir. All files found here will be copied and synchronized to site-root.
|
||||
# The assets-dir cannot have a sub directory with the same name/path as site-pkg-dir.
|
||||
#
|
||||
# Optional. Env: LEPTOS_ASSETS_DIR.
|
||||
assets-dir = "assets"
|
||||
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
|
||||
site-addr = "127.0.0.1:3007"
|
||||
# The port to use for automatic reload monitoring
|
||||
reload-port = 3001
|
||||
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
||||
# [Windows] for non-WSL use "npx.cmd playwright test"
|
||||
# This binary name can be checked in Powershell with Get-Command npx
|
||||
end2end-cmd = "npx playwright test"
|
||||
end2end-dir = "end2end"
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
browserquery = "defaults"
|
||||
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
|
||||
watch = false
|
||||
# The environment Leptos will run in, usually either "DEV" or "PROD"
|
||||
env = "DEV"
|
||||
# The features to use when compiling the bin target
|
||||
#
|
||||
# Optional. Can be over-ridden with the command line parameter --bin-features
|
||||
bin-features = ["ssr"]
|
||||
|
||||
# If the --no-default-features flag should be used when compiling the bin target
|
||||
#
|
||||
# Optional. Defaults to false.
|
||||
bin-default-features = false
|
||||
|
||||
# The features to use when compiling the lib target
|
||||
#
|
||||
# Optional. Can be over-ridden with the command line parameter --lib-features
|
||||
lib-features = ["hydrate"]
|
||||
|
||||
# If the --no-default-features flag should be used when compiling the lib target
|
||||
#
|
||||
# Optional. Defaults to false.
|
||||
lib-default-features = false
|
||||
|
||||
lib-profile-release = "wasm-release"
|
21
examples/static_routing/LICENSE
Normal file
21
examples/static_routing/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2022 henrik
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
8
examples/static_routing/Makefile.toml
Normal file
8
examples/static_routing/Makefile.toml
Normal file
|
@ -0,0 +1,8 @@
|
|||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/cargo-leptos.toml" },
|
||||
]
|
||||
|
||||
[env]
|
||||
|
||||
CLIENT_PROCESS_NAME = "ssr_modes_axum"
|
11
examples/static_routing/README.md
Normal file
11
examples/static_routing/README.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
# Static Routing Example
|
||||
|
||||
This example shows the static routing features, which can be used to generate the HTML content for some routes before a request.
|
||||
|
||||
## Getting Started
|
||||
|
||||
See the [Examples README](../README.md) for setup and run instructions.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Run `cargo leptos watch` to run this example.
|
BIN
examples/static_routing/assets/favicon.ico
Normal file
BIN
examples/static_routing/assets/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
3
examples/static_routing/posts/post1.md
Normal file
3
examples/static_routing/posts/post1.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# My first blog post
|
||||
|
||||
Having a blog is *fun*.
|
3
examples/static_routing/posts/post2.md
Normal file
3
examples/static_routing/posts/post2.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# My second blog post
|
||||
|
||||
Coming up with content is hard.
|
3
examples/static_routing/posts/post3.md
Normal file
3
examples/static_routing/posts/post3.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# My third blog post
|
||||
|
||||
Could I just have AI write this for me instead?
|
3
examples/static_routing/posts/post4.md
Normal file
3
examples/static_routing/posts/post4.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# My fourth post
|
||||
|
||||
Here is some content. It should regenerate the static page.
|
2
examples/static_routing/rust-toolchain.toml
Normal file
2
examples/static_routing/rust-toolchain.toml
Normal file
|
@ -0,0 +1,2 @@
|
|||
[toolchain]
|
||||
channel = "stable" # test change
|
323
examples/static_routing/src/app.rs
Normal file
323
examples/static_routing/src/app.rs
Normal file
|
@ -0,0 +1,323 @@
|
|||
use std::path::Path;
|
||||
|
||||
use futures::{channel::mpsc, Stream};
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::MetaTags;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::{
|
||||
components::{FlatRoutes, Redirect, Route, Router},
|
||||
hooks::use_params,
|
||||
params::Params,
|
||||
path,
|
||||
static_routes::StaticRoute,
|
||||
SsrMode,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
view! {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<AutoReload options=options.clone()/>
|
||||
<HydrationScripts options/>
|
||||
<MetaTags/>
|
||||
</head>
|
||||
<body>
|
||||
<App/>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
// Provides context that manages stylesheets, titles, meta tags, etc.
|
||||
provide_meta_context();
|
||||
let fallback = || view! { "Page not found." }.into_view();
|
||||
|
||||
view! {
|
||||
<Stylesheet id="leptos" href="/pkg/ssr_modes.css"/>
|
||||
<Title text="Welcome to Leptos"/>
|
||||
<Meta name="color-scheme" content="dark light"/>
|
||||
<Router>
|
||||
<nav>
|
||||
<a href="/">"Home"</a>
|
||||
</nav>
|
||||
<main>
|
||||
<FlatRoutes fallback>
|
||||
<Route
|
||||
path=path!("/")
|
||||
view=HomePage
|
||||
ssr=SsrMode::Static(
|
||||
StaticRoute::new().regenerate(|_| watch_path(Path::new("./posts"))),
|
||||
)
|
||||
/>
|
||||
|
||||
<Route
|
||||
path=path!("/about")
|
||||
view=move || view! { <Redirect path="/"/> }
|
||||
ssr=SsrMode::Static(StaticRoute::new())
|
||||
/>
|
||||
|
||||
<Route
|
||||
path=path!("/post/:slug/")
|
||||
view=Post
|
||||
ssr=SsrMode::Static(
|
||||
StaticRoute::new()
|
||||
.prerender_params(|| async move {
|
||||
[("slug".into(), list_slugs().await.unwrap_or_default())]
|
||||
.into_iter()
|
||||
.collect()
|
||||
})
|
||||
.regenerate(|params| {
|
||||
let slug = params.get("slug").unwrap();
|
||||
watch_path(Path::new(&format!("./posts/{slug}.md")))
|
||||
}),
|
||||
)
|
||||
/>
|
||||
|
||||
</FlatRoutes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn HomePage() -> impl IntoView {
|
||||
// load the posts
|
||||
let posts = Resource::new(|| (), |_| list_posts());
|
||||
let posts = move || {
|
||||
posts
|
||||
.get()
|
||||
.map(|n| n.unwrap_or_default())
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
view! {
|
||||
<h1>"My Great Blog"</h1>
|
||||
<Suspense fallback=move || view! { <p>"Loading posts..."</p> }>
|
||||
<ul>
|
||||
<For each=posts key=|post| post.slug.clone() let:post>
|
||||
<li>
|
||||
<a href=format!("/post/{}/", post.slug)>{post.title.clone()}</a>
|
||||
</li>
|
||||
</For>
|
||||
</ul>
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Params, Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PostParams {
|
||||
slug: Option<String>,
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Post() -> impl IntoView {
|
||||
let query = use_params::<PostParams>();
|
||||
let slug = move || {
|
||||
query
|
||||
.get()
|
||||
.map(|q| q.slug.unwrap_or_default())
|
||||
.map_err(|_| PostError::InvalidId)
|
||||
};
|
||||
let post_resource = Resource::new_blocking(slug, |slug| async move {
|
||||
match slug {
|
||||
Err(e) => Err(e),
|
||||
Ok(slug) => get_post(slug)
|
||||
.await
|
||||
.map(|data| data.ok_or(PostError::PostNotFound))
|
||||
.map_err(|e| PostError::ServerError(e.to_string())),
|
||||
}
|
||||
});
|
||||
|
||||
let post_view = move || {
|
||||
Suspend::new(async move {
|
||||
match post_resource.await {
|
||||
Ok(Ok(post)) => {
|
||||
Ok(view! {
|
||||
<h1>{post.title.clone()}</h1>
|
||||
<p>{post.content.clone()}</p>
|
||||
|
||||
// since we're using async rendering for this page,
|
||||
// this metadata should be included in the actual HTML <head>
|
||||
// when it's first served
|
||||
<Title text=post.title/>
|
||||
<Meta name="description" content=post.content/>
|
||||
})
|
||||
}
|
||||
Ok(Err(e)) | Err(e) => {
|
||||
Err(PostError::ServerError(e.to_string()))
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
view! {
|
||||
<em>"The world's best content."</em>
|
||||
<Suspense fallback=move || view! { <p>"Loading post..."</p> }>
|
||||
<ErrorBoundary fallback=|errors| {
|
||||
#[cfg(feature = "ssr")]
|
||||
expect_context::<leptos_axum::ResponseOptions>()
|
||||
.set_status(http::StatusCode::NOT_FOUND);
|
||||
view! {
|
||||
<div class="error">
|
||||
<h1>"Something went wrong."</h1>
|
||||
<ul>
|
||||
{move || {
|
||||
errors
|
||||
.get()
|
||||
.into_iter()
|
||||
.map(|(_, error)| view! { <li>{error.to_string()}</li> })
|
||||
.collect::<Vec<_>>()
|
||||
}}
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
}>{post_view}</ErrorBoundary>
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PostError {
|
||||
#[error("Invalid post ID.")]
|
||||
InvalidId,
|
||||
#[error("Post not found.")]
|
||||
PostNotFound,
|
||||
#[error("Server error: {0}.")]
|
||||
ServerError(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Post {
|
||||
slug: String,
|
||||
title: String,
|
||||
content: String,
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn list_slugs() -> Result<Vec<String>, ServerFnError> {
|
||||
use tokio::fs;
|
||||
use tokio_stream::wrappers::ReadDirStream;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
let files = ReadDirStream::new(fs::read_dir("./posts").await?);
|
||||
Ok(files
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
let path = entry.path();
|
||||
if !path.is_file() {
|
||||
return None;
|
||||
}
|
||||
let extension = path.extension()?;
|
||||
if extension != "md" {
|
||||
return None;
|
||||
}
|
||||
|
||||
let slug = path
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or_default()
|
||||
.replace(".md", "");
|
||||
Some(slug)
|
||||
})
|
||||
.collect()
|
||||
.await)
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn list_posts() -> Result<Vec<Post>, ServerFnError> {
|
||||
println!("calling list_posts");
|
||||
|
||||
use futures::TryStreamExt;
|
||||
use tokio::fs;
|
||||
use tokio_stream::wrappers::ReadDirStream;
|
||||
|
||||
let files = ReadDirStream::new(fs::read_dir("./posts").await?);
|
||||
files
|
||||
.try_filter_map(|entry| async move {
|
||||
let path = entry.path();
|
||||
if !path.is_file() {
|
||||
return Ok(None);
|
||||
}
|
||||
let Some(extension) = path.extension() else {
|
||||
return Ok(None);
|
||||
};
|
||||
if extension != "md" {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let slug = path
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or_default()
|
||||
.replace(".md", "");
|
||||
let content = fs::read_to_string(path).await?;
|
||||
// world's worst Markdown frontmatter parser
|
||||
let title = content.lines().next().unwrap().replace("# ", "");
|
||||
|
||||
Ok(Some(Post {
|
||||
slug,
|
||||
title,
|
||||
content,
|
||||
}))
|
||||
})
|
||||
.try_collect()
|
||||
.await
|
||||
.map_err(ServerFnError::from)
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn get_post(slug: String) -> Result<Option<Post>, ServerFnError> {
|
||||
println!("reading ./posts/{slug}.md");
|
||||
let content =
|
||||
tokio::fs::read_to_string(&format!("./posts/{slug}.md")).await?;
|
||||
// world's worst Markdown frontmatter parser
|
||||
let title = content.lines().next().unwrap().replace("# ", "");
|
||||
|
||||
Ok(Some(Post {
|
||||
slug,
|
||||
title,
|
||||
content,
|
||||
}))
|
||||
}
|
||||
|
||||
#[allow(unused)] // path is not used in non-SSR
|
||||
fn watch_path(path: &Path) -> impl Stream<Item = ()> {
|
||||
#[allow(unused)]
|
||||
let (mut tx, rx) = mpsc::channel(0);
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
use notify::RecursiveMode;
|
||||
use notify::Watcher;
|
||||
|
||||
let mut watcher =
|
||||
notify::recommended_watcher(move |res: Result<_, _>| {
|
||||
if res.is_ok() {
|
||||
// if this fails, it's because the buffer is full
|
||||
// this means we've already notified before it's regenerated,
|
||||
// so this page will be queued for regeneration already
|
||||
_ = tx.try_send(());
|
||||
}
|
||||
})
|
||||
.expect("could not create watcher");
|
||||
|
||||
// Add a path to be watched. All files and directories at that path and
|
||||
// below will be monitored for changes.
|
||||
watcher
|
||||
.watch(path, RecursiveMode::NonRecursive)
|
||||
.expect("could not watch path");
|
||||
|
||||
// we want this to run as long as the server is alive
|
||||
std::mem::forget(watcher);
|
||||
}
|
||||
|
||||
rx
|
||||
}
|
9
examples/static_routing/src/lib.rs
Normal file
9
examples/static_routing/src/lib.rs
Normal file
|
@ -0,0 +1,9 @@
|
|||
pub mod app;
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
use app::*;
|
||||
console_error_panic_hook::set_once();
|
||||
leptos::mount::hydrate_body(App);
|
||||
}
|
42
examples/static_routing/src/main.rs
Normal file
42
examples/static_routing/src/main.rs
Normal file
|
@ -0,0 +1,42 @@
|
|||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use axum::Router;
|
||||
use leptos::prelude::*;
|
||||
use leptos_axum::{generate_route_list_with_ssg, LeptosRoutes};
|
||||
use static_routing::app::*;
|
||||
|
||||
let conf = get_configuration(None).unwrap();
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
let leptos_options = conf.leptos_options;
|
||||
// Generate the list of routes in your Leptos App
|
||||
let (routes, static_routes) = generate_route_list_with_ssg({
|
||||
let leptos_options = leptos_options.clone();
|
||||
move || shell(leptos_options.clone())
|
||||
});
|
||||
|
||||
static_routes.generate(&leptos_options).await;
|
||||
|
||||
let app = Router::new()
|
||||
.leptos_routes(&leptos_options, routes, {
|
||||
let leptos_options = leptos_options.clone();
|
||||
move || shell(leptos_options.clone())
|
||||
})
|
||||
.fallback(leptos_axum::file_and_error_handler(shell))
|
||||
.with_state(leptos_options);
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
log!("listening on http://{}", &addr);
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn main() {
|
||||
// no client-side main function
|
||||
// unless we want this to work with e.g., Trunk for pure client-side testing
|
||||
// see lib.rs for hydration function instead
|
||||
}
|
3
examples/static_routing/style/main.scss
Normal file
3
examples/static_routing/style/main.scss
Normal file
|
@ -0,0 +1,3 @@
|
|||
body {
|
||||
font-family: sans-serif;
|
||||
}
|
|
@ -10,6 +10,7 @@ edition.workspace = true
|
|||
|
||||
[dependencies]
|
||||
actix-http = "3.8"
|
||||
actix-files = "0.6"
|
||||
actix-web = "4.8"
|
||||
futures = "0.3.30"
|
||||
any_spawner = { workspace = true, features = ["tokio"] }
|
||||
|
@ -25,6 +26,8 @@ parking_lot = "0.12.3"
|
|||
tracing = { version = "0.1", optional = true }
|
||||
tokio = { version = "1.39", features = ["rt", "fs"] }
|
||||
send_wrapper = "0.6.0"
|
||||
dashmap = "6"
|
||||
once_cell = "1"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--generate-link-to-definition"]
|
||||
|
|
|
@ -6,30 +6,38 @@
|
|||
//! [`examples`](https://github.com/leptos-rs/leptos/tree/main/examples)
|
||||
//! directory in the Leptos repository.
|
||||
|
||||
use actix_files::NamedFile;
|
||||
use actix_http::header::{HeaderName, HeaderValue, ACCEPT, LOCATION, REFERER};
|
||||
use actix_web::{
|
||||
body::BoxBody,
|
||||
dev::{ServiceFactory, ServiceRequest},
|
||||
http::header,
|
||||
web::{Payload, ServiceConfig},
|
||||
test,
|
||||
web::{Data, Payload, ServiceConfig},
|
||||
*,
|
||||
};
|
||||
use dashmap::DashMap;
|
||||
use futures::{stream::once, Stream, StreamExt};
|
||||
use http::StatusCode;
|
||||
use hydration_context::SsrSharedContext;
|
||||
use leptos::{
|
||||
config::LeptosOptions,
|
||||
context::{provide_context, use_context},
|
||||
prelude::expect_context,
|
||||
reactive_graph::{computed::ScopedFuture, owner::Owner},
|
||||
IntoView, *,
|
||||
IntoView,
|
||||
};
|
||||
use leptos_integration_utils::{
|
||||
BoxedFnOnce, ExtendResponse, PinnedFuture, PinnedStream,
|
||||
};
|
||||
use leptos_meta::ServerMetaContext;
|
||||
use leptos_router::{
|
||||
components::provide_server_redirect, location::RequestUrl, PathSegment,
|
||||
RouteList, RouteListing, SsrMode, StaticDataMap, StaticMode, *,
|
||||
components::provide_server_redirect,
|
||||
location::RequestUrl,
|
||||
static_routes::{RegenerationFn, ResolvedStaticPath},
|
||||
Method, PathSegment, RouteList, RouteListing, SsrMode,
|
||||
};
|
||||
use once_cell::sync::Lazy;
|
||||
use parking_lot::RwLock;
|
||||
use send_wrapper::SendWrapper;
|
||||
use server_fn::{
|
||||
|
@ -37,7 +45,9 @@ use server_fn::{
|
|||
};
|
||||
use std::{
|
||||
fmt::{Debug, Display},
|
||||
future::Future,
|
||||
ops::{Deref, DerefMut},
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
|
@ -728,13 +738,25 @@ pub fn render_app_async_with_context<IV>(
|
|||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
handle_response(method, additional_context, app_fn, |app, chunks| {
|
||||
Box::pin(async move {
|
||||
let app = app.to_html_stream_in_order().collect::<String>().await;
|
||||
let chunks = chunks();
|
||||
Box::pin(once(async move { app }).chain(chunks))
|
||||
as PinnedStream<String>
|
||||
})
|
||||
handle_response(method, additional_context, app_fn, async_stream_builder)
|
||||
}
|
||||
|
||||
fn async_stream_builder<IV>(
|
||||
app: IV,
|
||||
chunks: BoxedFnOnce<PinnedStream<String>>,
|
||||
) -> PinnedFuture<PinnedStream<String>>
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
Box::pin(async move {
|
||||
let app = if cfg!(feature = "islands-router") {
|
||||
app.to_html_stream_in_order_branching()
|
||||
} else {
|
||||
app.to_html_stream_in_order()
|
||||
};
|
||||
let app = app.collect::<String>().await;
|
||||
let chunks = chunks();
|
||||
Box::pin(once(async move { app }).chain(chunks)) as PinnedStream<String>
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -822,7 +844,7 @@ where
|
|||
/// 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 Fn() -> IV + 'static + Clone,
|
||||
app_fn: impl Fn() -> IV + 'static + Send + Clone,
|
||||
) -> Vec<ActixRouteListing>
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
|
@ -834,8 +856,8 @@ where
|
|||
/// 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<ActixRouteListing>, StaticDataMap)
|
||||
app_fn: impl Fn() -> IV + 'static + Send + Clone,
|
||||
) -> (Vec<ActixRouteListing>, StaticRouteGenerator)
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
|
@ -847,7 +869,7 @@ 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 Fn() -> IV + 'static + Clone,
|
||||
app_fn: impl Fn() -> IV + 'static + Send + Clone,
|
||||
excluded_routes: Option<Vec<String>>,
|
||||
) -> Vec<ActixRouteListing>
|
||||
where
|
||||
|
@ -861,9 +883,9 @@ 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_and_ssg<IV>(
|
||||
app_fn: impl Fn() -> IV + 'static + Clone,
|
||||
app_fn: impl Fn() -> IV + 'static + Send + Clone,
|
||||
excluded_routes: Option<Vec<String>>,
|
||||
) -> (Vec<ActixRouteListing>, StaticDataMap)
|
||||
) -> (Vec<ActixRouteListing>, StaticRouteGenerator)
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
|
@ -912,7 +934,7 @@ pub struct ActixRouteListing {
|
|||
path: String,
|
||||
mode: SsrMode,
|
||||
methods: Vec<leptos_router::Method>,
|
||||
static_mode: Option<(StaticMode, StaticDataMap)>,
|
||||
regenerate: Vec<RegenerationFn>,
|
||||
}
|
||||
|
||||
impl From<RouteListing> for ActixRouteListing {
|
||||
|
@ -925,12 +947,12 @@ impl From<RouteListing> for ActixRouteListing {
|
|||
};
|
||||
let mode = value.mode();
|
||||
let methods = value.methods().collect();
|
||||
let static_mode = value.into_static_parts();
|
||||
let regenerate = value.regenerate().into();
|
||||
Self {
|
||||
path,
|
||||
mode,
|
||||
mode: mode.clone(),
|
||||
methods,
|
||||
static_mode,
|
||||
regenerate,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -941,13 +963,13 @@ impl ActixRouteListing {
|
|||
path: String,
|
||||
mode: SsrMode,
|
||||
methods: impl IntoIterator<Item = leptos_router::Method>,
|
||||
static_mode: Option<(StaticMode, StaticDataMap)>,
|
||||
regenerate: impl Into<Vec<RegenerationFn>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
path,
|
||||
mode,
|
||||
methods: methods.into_iter().collect(),
|
||||
static_mode,
|
||||
regenerate: regenerate.into(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -958,19 +980,13 @@ impl ActixRouteListing {
|
|||
|
||||
/// The rendering mode for this path.
|
||||
pub fn mode(&self) -> SsrMode {
|
||||
self.mode
|
||||
self.mode.clone()
|
||||
}
|
||||
|
||||
/// The HTTP request methods this path can handle.
|
||||
pub fn methods(&self) -> impl Iterator<Item = leptos_router::Method> + '_ {
|
||||
self.methods.iter().copied()
|
||||
}
|
||||
|
||||
/// Whether this route is statically rendered.
|
||||
#[inline(always)]
|
||||
pub fn static_mode(&self) -> Option<StaticMode> {
|
||||
self.static_mode.as_ref().map(|n| n.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
|
||||
|
@ -979,10 +995,10 @@ impl ActixRouteListing {
|
|||
/// 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.
|
||||
/// Additional context will be provided to the app Element.
|
||||
pub fn generate_route_list_with_exclusions_and_ssg_and_context<IV>(
|
||||
app_fn: impl Fn() -> IV + 'static + Clone,
|
||||
app_fn: impl Fn() -> IV + 'static + Send + Clone,
|
||||
excluded_routes: Option<Vec<String>>,
|
||||
additional_context: impl Fn() + 'static + Clone,
|
||||
) -> (Vec<ActixRouteListing>, StaticDataMap)
|
||||
additional_context: impl Fn() + 'static + Send + Clone,
|
||||
) -> (Vec<ActixRouteListing>, StaticRouteGenerator)
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
|
@ -1001,6 +1017,12 @@ where
|
|||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let generator = StaticRouteGenerator::new(
|
||||
&routes,
|
||||
app_fn.clone(),
|
||||
additional_context.clone(),
|
||||
);
|
||||
|
||||
// Axum's Router defines Root routes as "/" not ""
|
||||
let mut routes = routes
|
||||
.into_inner()
|
||||
|
@ -1014,7 +1036,7 @@ where
|
|||
"/".to_string(),
|
||||
Default::default(),
|
||||
[leptos_router::Method::Get],
|
||||
None,
|
||||
vec![],
|
||||
)]
|
||||
} else {
|
||||
// Routes to exclude from auto generation
|
||||
|
@ -1024,192 +1046,251 @@ where
|
|||
}
|
||||
routes
|
||||
},
|
||||
StaticDataMap::new(), // TODO
|
||||
//static_data_map,
|
||||
generator,
|
||||
)
|
||||
}
|
||||
|
||||
/// Allows generating any prerendered routes.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub struct StaticRouteGenerator(
|
||||
Box<dyn FnOnce(&LeptosOptions) -> PinnedFuture<()> + Send>,
|
||||
);
|
||||
|
||||
impl StaticRouteGenerator {
|
||||
fn render_route<IV: IntoView + 'static>(
|
||||
path: String,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
additional_context: impl Fn() + Clone + Send + 'static,
|
||||
) -> impl Future<Output = (Owner, String)> {
|
||||
let (meta_context, meta_output) = ServerMetaContext::new();
|
||||
let additional_context = {
|
||||
let add_context = additional_context.clone();
|
||||
move || {
|
||||
let mock_req = test::TestRequest::with_uri(&path)
|
||||
.insert_header(("Accept", "text/html"))
|
||||
.to_http_request();
|
||||
let res_options = ResponseOptions::default();
|
||||
provide_contexts(
|
||||
Request::new(&mock_req),
|
||||
&meta_context,
|
||||
&res_options,
|
||||
);
|
||||
add_context();
|
||||
}
|
||||
};
|
||||
|
||||
let (owner, stream) = leptos_integration_utils::build_response(
|
||||
app_fn.clone(),
|
||||
additional_context,
|
||||
async_stream_builder,
|
||||
);
|
||||
|
||||
let sc = owner.shared_context().unwrap();
|
||||
|
||||
async move {
|
||||
let stream = stream.await;
|
||||
while let Some(pending) = sc.await_deferred() {
|
||||
pending.await;
|
||||
}
|
||||
|
||||
let html = meta_output
|
||||
.inject_meta_context(stream)
|
||||
.await
|
||||
.collect::<String>()
|
||||
.await;
|
||||
(owner, html)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new static route generator from the given list of route definitions.
|
||||
pub fn new<IV>(
|
||||
routes: &RouteList,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
additional_context: impl Fn() + Clone + Send + 'static,
|
||||
) -> Self
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
Self({
|
||||
let routes = routes.clone();
|
||||
Box::new(move |options| {
|
||||
let options = options.clone();
|
||||
let app_fn = app_fn.clone();
|
||||
let additional_context = additional_context.clone();
|
||||
|
||||
Box::pin(routes.generate_static_files(
|
||||
move |path: &ResolvedStaticPath| {
|
||||
Self::render_route(
|
||||
path.to_string(),
|
||||
app_fn.clone(),
|
||||
additional_context.clone(),
|
||||
)
|
||||
},
|
||||
move |path: &ResolvedStaticPath,
|
||||
owner: &Owner,
|
||||
html: String| {
|
||||
let options = options.clone();
|
||||
let path = path.to_owned();
|
||||
let response_options = owner.with(use_context);
|
||||
async move {
|
||||
write_static_route(
|
||||
&options,
|
||||
response_options,
|
||||
path.as_ref(),
|
||||
&html,
|
||||
)
|
||||
.await
|
||||
}
|
||||
},
|
||||
was_404,
|
||||
))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Generates the routes.
|
||||
pub async fn generate(self, options: &LeptosOptions) {
|
||||
(self.0)(options).await
|
||||
}
|
||||
}
|
||||
|
||||
static STATIC_HEADERS: Lazy<DashMap<String, ResponseOptions>> =
|
||||
Lazy::new(DashMap::new);
|
||||
|
||||
fn was_404(owner: &Owner) -> bool {
|
||||
let resp = owner.with(|| expect_context::<ResponseOptions>());
|
||||
let status = resp.0.read().status;
|
||||
|
||||
if let Some(status) = status {
|
||||
return status == StatusCode::NOT_FOUND;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn static_path(options: &LeptosOptions, path: &str) -> String {
|
||||
use leptos_integration_utils::static_file_path;
|
||||
|
||||
// If the path ends with a trailing slash, we generate the path
|
||||
// as a directory with a index.html file inside.
|
||||
if path != "/" && path.ends_with("/") {
|
||||
static_file_path(options, &format!("{}index", path))
|
||||
} else {
|
||||
static_file_path(options, path)
|
||||
}
|
||||
}
|
||||
|
||||
async fn write_static_route(
|
||||
options: &LeptosOptions,
|
||||
response_options: Option<ResponseOptions>,
|
||||
path: &str,
|
||||
html: &str,
|
||||
) -> Result<(), std::io::Error> {
|
||||
if let Some(options) = response_options {
|
||||
STATIC_HEADERS.insert(path.to_string(), options);
|
||||
}
|
||||
|
||||
let path = static_path(options, path);
|
||||
let path = Path::new(&path);
|
||||
if let Some(path) = path.parent() {
|
||||
tokio::fs::create_dir_all(path).await?;
|
||||
}
|
||||
tokio::fs::write(path, &html).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_static_route<IV>(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
regenerate: Vec<RegenerationFn>,
|
||||
) -> Route
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
let handler = move |req: HttpRequest, data: Data<LeptosOptions>| {
|
||||
Box::pin({
|
||||
let app_fn = app_fn.clone();
|
||||
let additional_context = additional_context.clone();
|
||||
let regenerate = regenerate.clone();
|
||||
async move {
|
||||
let options = data.into_inner();
|
||||
let orig_path = req.uri().path();
|
||||
let path = static_path(&options, orig_path);
|
||||
let path = Path::new(&path);
|
||||
let exists = tokio::fs::try_exists(path).await.unwrap_or(false);
|
||||
|
||||
let (response_options, html) = if !exists {
|
||||
let path = ResolvedStaticPath::new(orig_path);
|
||||
|
||||
let (owner, html) = path
|
||||
.build(
|
||||
move |path: &ResolvedStaticPath| {
|
||||
StaticRouteGenerator::render_route(
|
||||
path.to_string(),
|
||||
app_fn.clone(),
|
||||
additional_context.clone(),
|
||||
)
|
||||
},
|
||||
move |path: &ResolvedStaticPath,
|
||||
owner: &Owner,
|
||||
html: String| {
|
||||
let options = options.clone();
|
||||
let path = path.to_owned();
|
||||
let response_options = owner.with(use_context);
|
||||
async move {
|
||||
write_static_route(
|
||||
&options,
|
||||
response_options,
|
||||
path.as_ref(),
|
||||
&html,
|
||||
)
|
||||
.await
|
||||
}
|
||||
},
|
||||
was_404,
|
||||
regenerate,
|
||||
)
|
||||
.await;
|
||||
(owner.with(use_context::<ResponseOptions>), html)
|
||||
} else {
|
||||
let headers =
|
||||
STATIC_HEADERS.get(orig_path).map(|v| v.clone());
|
||||
(headers, None)
|
||||
};
|
||||
|
||||
// if html is Some(_), it means that `was_error_response` is true and we're not
|
||||
// actually going to cache this route, just return it as HTML
|
||||
//
|
||||
// this if for thing like 404s, where we do not want to cache an endless series of
|
||||
// typos (or malicious requests)
|
||||
let mut res = ActixResponse(match html {
|
||||
Some(html) => {
|
||||
HttpResponse::Ok().content_type("text/html").body(html)
|
||||
}
|
||||
None => match NamedFile::open(path) {
|
||||
Ok(res) => res.into_response(&req),
|
||||
Err(err) => HttpResponse::InternalServerError()
|
||||
.body(err.to_string()),
|
||||
},
|
||||
});
|
||||
|
||||
if let Some(options) = response_options {
|
||||
res.extend_response(&options);
|
||||
}
|
||||
|
||||
res.0
|
||||
}
|
||||
})
|
||||
};
|
||||
web::get().to(handler)
|
||||
}
|
||||
|
||||
pub enum DataResponse<T> {
|
||||
Data(T),
|
||||
Response(actix_web::dev::Response<BoxBody>),
|
||||
}
|
||||
|
||||
// TODO static response
|
||||
/*
|
||||
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 {
|
||||
|
@ -1290,19 +1371,15 @@ where
|
|||
provide_context(method);
|
||||
additional_context();
|
||||
};
|
||||
router = if let Some(static_mode) = listing.static_mode() {
|
||||
_ = static_mode;
|
||||
todo!() /*
|
||||
router.route(
|
||||
path,
|
||||
static_route(
|
||||
app_fn.clone(),
|
||||
additional_context_and_method.clone(),
|
||||
method,
|
||||
static_mode,
|
||||
),
|
||||
)
|
||||
*/
|
||||
router = if matches!(listing.mode(), SsrMode::Static(_)) {
|
||||
router.route(
|
||||
path,
|
||||
handle_static_route(
|
||||
additional_context_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
listing.regenerate.clone(),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
router.route(
|
||||
path,
|
||||
|
@ -1334,6 +1411,7 @@ where
|
|||
app_fn.clone(),
|
||||
method,
|
||||
),
|
||||
_ => unreachable!()
|
||||
},
|
||||
)
|
||||
};
|
||||
|
@ -1390,7 +1468,17 @@ impl LeptosRoutes for &mut ServiceConfig {
|
|||
let mode = listing.mode();
|
||||
|
||||
for method in listing.methods() {
|
||||
router = router.route(
|
||||
if matches!(listing.mode(), SsrMode::Static(_)) {
|
||||
router = router.route(
|
||||
path,
|
||||
handle_static_route(
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
listing.regenerate.clone(),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
router = router.route(
|
||||
path,
|
||||
match mode {
|
||||
SsrMode::OutOfOrder => {
|
||||
|
@ -1420,8 +1508,10 @@ impl LeptosRoutes for &mut ServiceConfig {
|
|||
app_fn.clone(),
|
||||
method,
|
||||
),
|
||||
_ => unreachable!()
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ hydration_context = { workspace = true }
|
|||
axum = { version = "0.7.5", default-features = false, features = [
|
||||
"matched-path",
|
||||
] }
|
||||
dashmap = "6"
|
||||
futures = "0.3.30"
|
||||
http = "1.1"
|
||||
http-body-util = "0.1.2"
|
||||
|
@ -23,6 +24,7 @@ leptos_macro = { workspace = true, features = ["axum"] }
|
|||
leptos_meta = { workspace = true, features = ["ssr"] }
|
||||
leptos_router = { workspace = true, features = ["ssr"] }
|
||||
leptos_integration_utils = { workspace = true }
|
||||
once_cell = "1"
|
||||
parking_lot = "0.12.3"
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1.39", default-features = false }
|
||||
|
|
|
@ -32,9 +32,11 @@
|
|||
//! [`examples`](https://github.com/leptos-rs/leptos/tree/main/examples)
|
||||
//! directory in the Leptos repository.
|
||||
|
||||
#[cfg(feature = "default")]
|
||||
use axum::http::Uri;
|
||||
use axum::{
|
||||
body::{Body, Bytes},
|
||||
extract::{FromRequestParts, MatchedPath},
|
||||
extract::{FromRef, FromRequestParts, MatchedPath, State},
|
||||
http::{
|
||||
header::{self, HeaderName, HeaderValue, ACCEPT, LOCATION, REFERER},
|
||||
request::Parts,
|
||||
|
@ -44,10 +46,7 @@ use axum::{
|
|||
routing::{delete, get, patch, post, put},
|
||||
};
|
||||
#[cfg(feature = "default")]
|
||||
use axum::{
|
||||
extract::{FromRef, State},
|
||||
http::Uri,
|
||||
};
|
||||
use dashmap::DashMap;
|
||||
use futures::{stream::once, Future, Stream, StreamExt};
|
||||
use hydration_context::SsrSharedContext;
|
||||
use leptos::{
|
||||
|
@ -61,12 +60,20 @@ use leptos_integration_utils::{
|
|||
BoxedFnOnce, ExtendResponse, PinnedFuture, PinnedStream,
|
||||
};
|
||||
use leptos_meta::ServerMetaContext;
|
||||
#[cfg(feature = "default")]
|
||||
use leptos_router::static_routes::ResolvedStaticPath;
|
||||
use leptos_router::{
|
||||
components::provide_server_redirect, location::RequestUrl, PathSegment,
|
||||
RouteList, RouteListing, SsrMode, StaticDataMap, StaticMode,
|
||||
components::provide_server_redirect,
|
||||
location::RequestUrl,
|
||||
static_routes::{RegenerationFn, StaticParamsMap},
|
||||
PathSegment, RouteList, RouteListing, SsrMode,
|
||||
};
|
||||
#[cfg(feature = "default")]
|
||||
use once_cell::sync::Lazy;
|
||||
use parking_lot::RwLock;
|
||||
use server_fn::{redirect::REDIRECT_HEADER, ServerFnError};
|
||||
#[cfg(feature = "default")]
|
||||
use std::path::Path;
|
||||
use std::{fmt::Debug, io, pin::Pin, sync::Arc};
|
||||
#[cfg(feature = "default")]
|
||||
use tower::ServiceExt;
|
||||
|
@ -236,14 +243,20 @@ pub fn redirect(path: &str) {
|
|||
);
|
||||
}
|
||||
} else {
|
||||
let msg = "Couldn't retrieve either Parts or ResponseOptions while \
|
||||
trying to redirect().";
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::warn!("{}", &msg);
|
||||
|
||||
{
|
||||
tracing::warn!(
|
||||
"Couldn't retrieve either Parts or ResponseOptions while \
|
||||
trying to redirect()."
|
||||
);
|
||||
}
|
||||
#[cfg(not(feature = "tracing"))]
|
||||
eprintln!("{}", &msg);
|
||||
{
|
||||
eprintln!(
|
||||
"Couldn't retrieve either Parts or ResponseOptions while \
|
||||
trying to redirect()."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -497,10 +510,11 @@ where
|
|||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_route<IV>(
|
||||
pub fn render_route<S, IV>(
|
||||
paths: Vec<AxumRouteListing>,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
) -> impl Fn(
|
||||
State<S>,
|
||||
Request<Body>,
|
||||
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
|
||||
+ Clone
|
||||
|
@ -508,6 +522,8 @@ pub fn render_route<IV>(
|
|||
+ 'static
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
LeptosOptions: FromRef<S>,
|
||||
S: Send + 'static,
|
||||
{
|
||||
render_route_with_context(paths, || {}, app_fn)
|
||||
}
|
||||
|
@ -648,11 +664,12 @@ where
|
|||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_route_with_context<IV>(
|
||||
pub fn render_route_with_context<S, IV>(
|
||||
paths: Vec<AxumRouteListing>,
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
) -> impl Fn(
|
||||
State<S>,
|
||||
Request<Body>,
|
||||
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
|
||||
+ Clone
|
||||
|
@ -660,6 +677,8 @@ pub fn render_route_with_context<IV>(
|
|||
+ 'static
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
LeptosOptions: FromRef<S>,
|
||||
S: Send + 'static,
|
||||
{
|
||||
let ooo = render_app_to_stream_with_context(
|
||||
additional_context.clone(),
|
||||
|
@ -679,7 +698,7 @@ where
|
|||
app_fn.clone(),
|
||||
);
|
||||
|
||||
move |req| {
|
||||
move |state, req| {
|
||||
// 1. Process route to match the values in routeListing
|
||||
let path = req
|
||||
.extensions()
|
||||
|
@ -702,6 +721,25 @@ where
|
|||
SsrMode::PartiallyBlocked => pb(req),
|
||||
SsrMode::InOrder => io(req),
|
||||
SsrMode::Async => asyn(req),
|
||||
SsrMode::Static(_) => {
|
||||
#[cfg(feature = "default")]
|
||||
{
|
||||
let regenerate = listing.regenerate.clone();
|
||||
handle_static_route(
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
regenerate,
|
||||
)(state, req)
|
||||
}
|
||||
#[cfg(not(feature = "default"))]
|
||||
{
|
||||
_ = state;
|
||||
panic!(
|
||||
"Static routes are not currently supported on WASM32 \
|
||||
server targets."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1097,18 +1135,25 @@ pub fn render_app_async_with_context<IV>(
|
|||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
handle_response(additional_context, app_fn, |app, chunks| {
|
||||
Box::pin(async move {
|
||||
let app = if cfg!(feature = "islands-router") {
|
||||
app.to_html_stream_in_order_branching()
|
||||
} else {
|
||||
app.to_html_stream_in_order()
|
||||
};
|
||||
let app = app.collect::<String>().await;
|
||||
let chunks = chunks();
|
||||
Box::pin(once(async move { app }).chain(chunks))
|
||||
as PinnedStream<String>
|
||||
})
|
||||
handle_response(additional_context, app_fn, async_stream_builder)
|
||||
}
|
||||
|
||||
fn async_stream_builder<IV>(
|
||||
app: IV,
|
||||
chunks: BoxedFnOnce<PinnedStream<String>>,
|
||||
) -> PinnedFuture<PinnedStream<String>>
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
Box::pin(async move {
|
||||
let app = if cfg!(feature = "islands-router") {
|
||||
app.to_html_stream_in_order_branching()
|
||||
} else {
|
||||
app.to_html_stream_in_order()
|
||||
};
|
||||
let app = app.collect::<String>().await;
|
||||
let chunks = chunks();
|
||||
Box::pin(once(async move { app }).chain(chunks)) as PinnedStream<String>
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -1120,7 +1165,7 @@ where
|
|||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn generate_route_list<IV>(
|
||||
app_fn: impl Fn() -> IV + 'static + Clone,
|
||||
app_fn: impl Fn() -> IV + 'static + Clone + Send,
|
||||
) -> Vec<AxumRouteListing>
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
|
@ -1128,7 +1173,7 @@ where
|
|||
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
|
||||
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use t.clone()his 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.
|
||||
#[cfg_attr(
|
||||
|
@ -1136,8 +1181,8 @@ where
|
|||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn generate_route_list_with_ssg<IV>(
|
||||
app_fn: impl Fn() -> IV + 'static + Clone,
|
||||
) -> (Vec<AxumRouteListing>, StaticDataMap)
|
||||
app_fn: impl Fn() -> IV + 'static + Clone + Send,
|
||||
) -> (Vec<AxumRouteListing>, StaticRouteGenerator)
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
|
@ -1153,7 +1198,7 @@ where
|
|||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn generate_route_list_with_exclusions<IV>(
|
||||
app_fn: impl Fn() -> IV + 'static + Clone,
|
||||
app_fn: impl Fn() -> IV + 'static + Clone + Send,
|
||||
excluded_routes: Option<Vec<String>>,
|
||||
) -> Vec<AxumRouteListing>
|
||||
where
|
||||
|
@ -1162,13 +1207,13 @@ where
|
|||
generate_route_list_with_exclusions_and_ssg(app_fn, excluded_routes).0
|
||||
}
|
||||
|
||||
/// TODO docs
|
||||
/// Builds all routes that have been defined using [`StaticRoute`].
|
||||
#[allow(unused)]
|
||||
pub async fn build_static_routes<IV>(
|
||||
options: &LeptosOptions,
|
||||
app_fn: impl Fn() -> IV + 'static + Send + Clone,
|
||||
routes: &[RouteListing],
|
||||
static_data_map: StaticDataMap,
|
||||
static_data_map: StaticParamsMap,
|
||||
) where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
|
@ -1197,9 +1242,9 @@ pub async fn build_static_routes<IV>(
|
|||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn generate_route_list_with_exclusions_and_ssg<IV>(
|
||||
app_fn: impl Fn() -> IV + 'static + Clone,
|
||||
app_fn: impl Fn() -> IV + 'static + Clone + Send,
|
||||
excluded_routes: Option<Vec<String>>,
|
||||
) -> (Vec<AxumRouteListing>, StaticDataMap)
|
||||
) -> (Vec<AxumRouteListing>, StaticRouteGenerator)
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
|
@ -1216,7 +1261,8 @@ pub struct AxumRouteListing {
|
|||
path: String,
|
||||
mode: SsrMode,
|
||||
methods: Vec<leptos_router::Method>,
|
||||
static_mode: Option<(StaticMode, StaticDataMap)>,
|
||||
#[allow(unused)]
|
||||
regenerate: Vec<RegenerationFn>,
|
||||
}
|
||||
|
||||
impl From<RouteListing> for AxumRouteListing {
|
||||
|
@ -1229,12 +1275,12 @@ impl From<RouteListing> for AxumRouteListing {
|
|||
};
|
||||
let mode = value.mode();
|
||||
let methods = value.methods().collect();
|
||||
let static_mode = value.into_static_parts();
|
||||
let regenerate = value.regenerate().into();
|
||||
Self {
|
||||
path,
|
||||
mode,
|
||||
mode: mode.clone(),
|
||||
methods,
|
||||
static_mode,
|
||||
regenerate,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1245,13 +1291,13 @@ impl AxumRouteListing {
|
|||
path: String,
|
||||
mode: SsrMode,
|
||||
methods: impl IntoIterator<Item = leptos_router::Method>,
|
||||
static_mode: Option<(StaticMode, StaticDataMap)>,
|
||||
regenerate: impl Into<Vec<RegenerationFn>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
path,
|
||||
mode,
|
||||
methods: methods.into_iter().collect(),
|
||||
static_mode,
|
||||
regenerate: regenerate.into(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1261,20 +1307,14 @@ impl AxumRouteListing {
|
|||
}
|
||||
|
||||
/// The rendering mode for this path.
|
||||
pub fn mode(&self) -> SsrMode {
|
||||
self.mode
|
||||
pub fn mode(&self) -> &SsrMode {
|
||||
&self.mode
|
||||
}
|
||||
|
||||
/// The HTTP request methods this path can handle.
|
||||
pub fn methods(&self) -> impl Iterator<Item = leptos_router::Method> + '_ {
|
||||
self.methods.iter().copied()
|
||||
}
|
||||
|
||||
/// Whether this route is statically rendered.
|
||||
#[inline(always)]
|
||||
pub fn static_mode(&self) -> Option<StaticMode> {
|
||||
self.static_mode.as_ref().map(|n| n.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
|
||||
|
@ -1287,16 +1327,17 @@ impl AxumRouteListing {
|
|||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn generate_route_list_with_exclusions_and_ssg_and_context<IV>(
|
||||
app_fn: impl Fn() -> IV + 'static + Clone,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
excluded_routes: Option<Vec<String>>,
|
||||
additional_context: impl Fn() + 'static + Clone,
|
||||
) -> (Vec<AxumRouteListing>, StaticDataMap)
|
||||
additional_context: impl Fn() + Clone + Send + 'static,
|
||||
) -> (Vec<AxumRouteListing>, StaticRouteGenerator)
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
// do some basic reactive setup
|
||||
init_executor();
|
||||
|
||||
let owner = Owner::new_root(Some(Arc::new(SsrSharedContext::new())));
|
||||
|
||||
let routes = owner
|
||||
.with(|| {
|
||||
// stub out a path for now
|
||||
|
@ -1310,6 +1351,12 @@ where
|
|||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let generator = StaticRouteGenerator::new(
|
||||
&routes,
|
||||
app_fn.clone(),
|
||||
additional_context.clone(),
|
||||
);
|
||||
|
||||
// Axum's Router defines Root routes as "/" not ""
|
||||
let mut routes = routes
|
||||
.into_inner()
|
||||
|
@ -1323,7 +1370,7 @@ where
|
|||
"/".to_string(),
|
||||
Default::default(),
|
||||
[leptos_router::Method::Get],
|
||||
None,
|
||||
vec![],
|
||||
)]
|
||||
} else {
|
||||
// Routes to exclude from auto generation
|
||||
|
@ -1333,16 +1380,284 @@ where
|
|||
}
|
||||
routes
|
||||
},
|
||||
StaticDataMap::new(), // TODO
|
||||
//static_data_map,
|
||||
generator,
|
||||
)
|
||||
}
|
||||
|
||||
/// Allows generating any prerendered routes.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub struct StaticRouteGenerator(
|
||||
Box<dyn FnOnce(&LeptosOptions) -> PinnedFuture<()> + Send>,
|
||||
);
|
||||
|
||||
impl StaticRouteGenerator {
|
||||
#[cfg(feature = "default")]
|
||||
fn render_route<IV: IntoView + 'static>(
|
||||
path: String,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
additional_context: impl Fn() + Clone + Send + 'static,
|
||||
) -> impl Future<Output = (Owner, String)> {
|
||||
let (meta_context, meta_output) = ServerMetaContext::new();
|
||||
let additional_context = {
|
||||
let add_context = additional_context.clone();
|
||||
move || {
|
||||
let full_path = format!("http://leptos.dev{path}");
|
||||
let mock_req = http::Request::builder()
|
||||
.method(http::Method::GET)
|
||||
.header("Accept", "text/html")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let (mock_parts, _) = mock_req.into_parts();
|
||||
let res_options = ResponseOptions::default();
|
||||
provide_contexts(
|
||||
&full_path,
|
||||
&meta_context,
|
||||
mock_parts,
|
||||
res_options,
|
||||
);
|
||||
add_context();
|
||||
}
|
||||
};
|
||||
|
||||
let (owner, stream) = leptos_integration_utils::build_response(
|
||||
app_fn.clone(),
|
||||
additional_context,
|
||||
async_stream_builder,
|
||||
);
|
||||
|
||||
let sc = owner.shared_context().unwrap();
|
||||
|
||||
async move {
|
||||
let stream = stream.await;
|
||||
while let Some(pending) = sc.await_deferred() {
|
||||
pending.await;
|
||||
}
|
||||
|
||||
let html = meta_output
|
||||
.inject_meta_context(stream)
|
||||
.await
|
||||
.collect::<String>()
|
||||
.await;
|
||||
(owner, html)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new static route generator from the given list of route definitions.
|
||||
pub fn new<IV>(
|
||||
routes: &RouteList,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
additional_context: impl Fn() + Clone + Send + 'static,
|
||||
) -> Self
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
#[cfg(feature = "default")]
|
||||
{
|
||||
Self({
|
||||
let routes = routes.clone();
|
||||
Box::new(move |options| {
|
||||
let options = options.clone();
|
||||
let app_fn = app_fn.clone();
|
||||
let additional_context = additional_context.clone();
|
||||
|
||||
Box::pin(routes.generate_static_files(
|
||||
move |path: &ResolvedStaticPath| {
|
||||
Self::render_route(
|
||||
path.to_string(),
|
||||
app_fn.clone(),
|
||||
additional_context.clone(),
|
||||
)
|
||||
},
|
||||
move |path: &ResolvedStaticPath,
|
||||
owner: &Owner,
|
||||
html: String| {
|
||||
let options = options.clone();
|
||||
let path = path.to_owned();
|
||||
let response_options = owner.with(use_context);
|
||||
async move {
|
||||
write_static_route(
|
||||
&options,
|
||||
response_options,
|
||||
path.as_ref(),
|
||||
&html,
|
||||
)
|
||||
.await
|
||||
}
|
||||
},
|
||||
was_404,
|
||||
))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "default"))]
|
||||
{
|
||||
_ = routes;
|
||||
_ = app_fn;
|
||||
_ = additional_context;
|
||||
panic!(
|
||||
"Static routes are not currently supported on WASM32 server \
|
||||
targets."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates the routes.
|
||||
pub async fn generate(self, options: &LeptosOptions) {
|
||||
(self.0)(options).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "default")]
|
||||
static STATIC_HEADERS: Lazy<DashMap<String, ResponseOptions>> =
|
||||
Lazy::new(DashMap::new);
|
||||
|
||||
#[cfg(feature = "default")]
|
||||
fn was_404(owner: &Owner) -> bool {
|
||||
let resp = owner.with(|| expect_context::<ResponseOptions>());
|
||||
let status = resp.0.read().status;
|
||||
|
||||
if let Some(status) = status {
|
||||
return status == StatusCode::NOT_FOUND;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(feature = "default")]
|
||||
fn static_path(options: &LeptosOptions, path: &str) -> String {
|
||||
use leptos_integration_utils::static_file_path;
|
||||
|
||||
// If the path ends with a trailing slash, we generate the path
|
||||
// as a directory with a index.html file inside.
|
||||
if path != "/" && path.ends_with("/") {
|
||||
static_file_path(options, &format!("{}index", path))
|
||||
} else {
|
||||
static_file_path(options, path)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "default")]
|
||||
async fn write_static_route(
|
||||
options: &LeptosOptions,
|
||||
response_options: Option<ResponseOptions>,
|
||||
path: &str,
|
||||
html: &str,
|
||||
) -> Result<(), std::io::Error> {
|
||||
if let Some(options) = response_options {
|
||||
STATIC_HEADERS.insert(path.to_string(), options);
|
||||
}
|
||||
|
||||
let path = static_path(options, path);
|
||||
let path = Path::new(&path);
|
||||
if let Some(path) = path.parent() {
|
||||
tokio::fs::create_dir_all(path).await?;
|
||||
}
|
||||
tokio::fs::write(path, &html).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "default")]
|
||||
fn handle_static_route<S, IV>(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
regenerate: Vec<RegenerationFn>,
|
||||
) -> impl Fn(
|
||||
State<S>,
|
||||
Request<Body>,
|
||||
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
|
||||
+ Clone
|
||||
+ Send
|
||||
+ 'static
|
||||
where
|
||||
LeptosOptions: FromRef<S>,
|
||||
S: Send + 'static,
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
use tower_http::services::ServeFile;
|
||||
|
||||
move |state, req| {
|
||||
let app_fn = app_fn.clone();
|
||||
let additional_context = additional_context.clone();
|
||||
let regenerate = regenerate.clone();
|
||||
Box::pin(async move {
|
||||
let options = LeptosOptions::from_ref(&state);
|
||||
let orig_path = req.uri().path();
|
||||
let path = static_path(&options, orig_path);
|
||||
let path = Path::new(&path);
|
||||
let exists = tokio::fs::try_exists(path).await.unwrap_or(false);
|
||||
|
||||
let (response_options, html) = if !exists {
|
||||
let path = ResolvedStaticPath::new(orig_path);
|
||||
|
||||
let (owner, html) = path
|
||||
.build(
|
||||
move |path: &ResolvedStaticPath| {
|
||||
StaticRouteGenerator::render_route(
|
||||
path.to_string(),
|
||||
app_fn.clone(),
|
||||
additional_context.clone(),
|
||||
)
|
||||
},
|
||||
move |path: &ResolvedStaticPath,
|
||||
owner: &Owner,
|
||||
html: String| {
|
||||
let options = options.clone();
|
||||
let path = path.to_owned();
|
||||
let response_options = owner.with(use_context);
|
||||
async move {
|
||||
write_static_route(
|
||||
&options,
|
||||
response_options,
|
||||
path.as_ref(),
|
||||
&html,
|
||||
)
|
||||
.await
|
||||
}
|
||||
},
|
||||
was_404,
|
||||
regenerate,
|
||||
)
|
||||
.await;
|
||||
(owner.with(use_context::<ResponseOptions>), html)
|
||||
} else {
|
||||
let headers = STATIC_HEADERS.get(orig_path).map(|v| v.clone());
|
||||
(headers, None)
|
||||
};
|
||||
|
||||
// if html is Some(_), it means that `was_error_response` is true and we're not
|
||||
// actually going to cache this route, just return it as HTML
|
||||
//
|
||||
// this if for thing like 404s, where we do not want to cache an endless series of
|
||||
// typos (or malicious requests)
|
||||
let mut res = AxumResponse(match html {
|
||||
Some(html) => axum::response::Html(html).into_response(),
|
||||
None => match ServeFile::new(path).oneshot(req).await {
|
||||
Ok(res) => res.into_response(),
|
||||
Err(err) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {err}"),
|
||||
)
|
||||
.into_response(),
|
||||
},
|
||||
});
|
||||
|
||||
if let Some(options) = response_options {
|
||||
res.extend_response(&options);
|
||||
}
|
||||
|
||||
res.0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// This trait allows one to pass a list of routes and a render function to Axum's router, letting us avoid
|
||||
/// having to use wildcards or manually define all routes in multiple places.
|
||||
pub trait LeptosRoutes<S>
|
||||
where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
LeptosOptions: FromRef<S>,
|
||||
{
|
||||
fn leptos_routes<IV>(
|
||||
self,
|
||||
|
@ -1372,209 +1687,6 @@ where
|
|||
H: axum::handler::Handler<T, S>,
|
||||
T: 'static;
|
||||
}
|
||||
/*
|
||||
#[cfg(feature = "default")]
|
||||
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
|
||||
}
|
||||
}
|
||||
})
|
||||
}*/
|
||||
|
||||
#[allow(unused)] // TODO
|
||||
#[cfg(feature = "default")]
|
||||
fn static_route<IV, S>(
|
||||
router: axum::Router<S>,
|
||||
path: &str,
|
||||
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,
|
||||
{
|
||||
todo!()
|
||||
/*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();
|
||||
spawn_task!(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();
|
||||
spawn_task!(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),
|
||||
},
|
||||
)
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
trait AxumPath {
|
||||
fn to_axum_path(&self) -> String;
|
||||
|
@ -1611,6 +1723,7 @@ impl AxumPath for &[PathSegment] {
|
|||
impl<S> LeptosRoutes<S> for axum::Router<S>
|
||||
where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
LeptosOptions: FromRef<S>,
|
||||
{
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
|
@ -1688,25 +1801,24 @@ where
|
|||
provide_context(method);
|
||||
cx_with_state();
|
||||
};
|
||||
router = if let Some(static_mode) = listing.static_mode() {
|
||||
router = if matches!(listing.mode(), SsrMode::Static(_)) {
|
||||
#[cfg(feature = "default")]
|
||||
{
|
||||
static_route(
|
||||
router,
|
||||
router.route(
|
||||
path,
|
||||
app_fn.clone(),
|
||||
cx_with_state_and_method.clone(),
|
||||
method,
|
||||
static_mode,
|
||||
get(handle_static_route(
|
||||
cx_with_state_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
listing.regenerate.clone(),
|
||||
)),
|
||||
)
|
||||
}
|
||||
#[cfg(not(feature = "default"))]
|
||||
{
|
||||
_ = static_mode;
|
||||
panic!(
|
||||
"Static site generation is not currently \
|
||||
supported on WASM32 server targets."
|
||||
)
|
||||
"Static routes are not currently supported on \
|
||||
WASM32 server targets."
|
||||
);
|
||||
}
|
||||
} else {
|
||||
router.route(
|
||||
|
@ -1765,6 +1877,7 @@ where
|
|||
leptos_router::Method::Patch => patch(s),
|
||||
}
|
||||
}
|
||||
_ => unreachable!()
|
||||
},
|
||||
)
|
||||
};
|
||||
|
|
|
@ -5,6 +5,7 @@ use leptos::{
|
|||
reactive_graph::owner::{Owner, Sandboxed},
|
||||
IntoView,
|
||||
};
|
||||
use leptos_config::LeptosOptions;
|
||||
use leptos_meta::ServerMetaContextOutput;
|
||||
use std::{future::Future, pin::Pin, sync::Arc};
|
||||
|
||||
|
@ -132,3 +133,13 @@ where
|
|||
}));
|
||||
(owner, stream)
|
||||
}
|
||||
|
||||
pub fn static_file_path(options: &LeptosOptions, path: &str) -> String {
|
||||
let trimmed_path = path.trim_start_matches('/');
|
||||
let path = if trimmed_path.is_empty() {
|
||||
"index"
|
||||
} else {
|
||||
trimmed_path
|
||||
};
|
||||
format!("{}/{}.html", options.site_root, path)
|
||||
}
|
||||
|
|
|
@ -264,7 +264,6 @@ where
|
|||
{
|
||||
buf.next_id();
|
||||
let suspense_context = use_context::<SuspenseContext>().unwrap();
|
||||
|
||||
let owner = Owner::current().unwrap();
|
||||
|
||||
// we need to wait for one of two things: either
|
||||
|
@ -277,6 +276,16 @@ where
|
|||
futures::channel::oneshot::channel::<()>();
|
||||
|
||||
let mut tasks_tx = Some(tasks_tx);
|
||||
|
||||
// now, create listener for local resources
|
||||
let (local_tx, mut local_rx) =
|
||||
futures::channel::oneshot::channel::<()>();
|
||||
provide_context(LocalResourceNotifier::from(local_tx));
|
||||
|
||||
// walk over the tree of children once to make sure that all resource loads are registered
|
||||
self.children.dry_resolve();
|
||||
|
||||
// check the set of tasks to see if it is empty, now or later
|
||||
let eff = reactive_graph::effect::RenderEffect::new_isomorphic({
|
||||
move |_| {
|
||||
tasks.track();
|
||||
|
@ -290,14 +299,6 @@ where
|
|||
}
|
||||
});
|
||||
|
||||
// now, create listener for local resources
|
||||
let (local_tx, mut local_rx) =
|
||||
futures::channel::oneshot::channel::<()>();
|
||||
provide_context(LocalResourceNotifier::from(local_tx));
|
||||
|
||||
// walk over the tree of children once to make sure that all resource loads are registered
|
||||
self.children.dry_resolve();
|
||||
|
||||
let mut fut = Box::pin(ScopedFuture::new(ErrorHookFuture::new(
|
||||
async move {
|
||||
// race the local resource notifier against the set of tasks
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
use crate::owner::Owner;
|
||||
use or_poisoned::OrPoisoned;
|
||||
use std::any::{Any, TypeId};
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
collections::VecDeque,
|
||||
};
|
||||
|
||||
impl Owner {
|
||||
fn provide_context<T: Send + Sync + 'static>(&self, value: T) {
|
||||
|
@ -60,6 +63,35 @@ impl Owner {
|
|||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Searches for items stored in context in either direction, either among parents or among
|
||||
/// descendants.
|
||||
pub fn use_context_bidirectional<T: Clone + 'static>(&self) -> Option<T> {
|
||||
self.use_context()
|
||||
.unwrap_or_else(|| self.find_context_in_children())
|
||||
}
|
||||
|
||||
fn find_context_in_children<T: Clone + 'static>(&self) -> Option<T> {
|
||||
let ty = TypeId::of::<T>();
|
||||
let inner = self.inner.read().or_poisoned();
|
||||
let mut to_search = VecDeque::new();
|
||||
to_search.extend(inner.children.clone());
|
||||
drop(inner);
|
||||
|
||||
while let Some(next) = to_search.pop_front() {
|
||||
if let Some(child) = next.upgrade() {
|
||||
let child = child.read().or_poisoned();
|
||||
let contexts = &child.contexts;
|
||||
if let Some(context) = contexts.get(&ty) {
|
||||
return context.downcast_ref::<T>().cloned();
|
||||
}
|
||||
|
||||
to_search.extend(child.children.clone());
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides a context value of type `T` to the current reactive [`Owner`]
|
||||
|
|
|
@ -28,6 +28,7 @@ send_wrapper = "0.6.0"
|
|||
thiserror = "1.0"
|
||||
percent-encoding = { version = "2.3", optional = true }
|
||||
gloo-net = "0.6.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3.70"
|
||||
|
|
|
@ -450,6 +450,12 @@ pub fn Redirect<P>(
|
|||
"Calling <Redirect/> without a ServerRedirectFunction \
|
||||
provided, in SSR mode."
|
||||
);
|
||||
|
||||
#[cfg(not(feature = "tracing"))]
|
||||
eprintln!(
|
||||
"Calling <Redirect/> without a ServerRedirectFunction \
|
||||
provided, in SSR mode."
|
||||
);
|
||||
return;
|
||||
}
|
||||
let navigate = use_navigate();
|
||||
|
|
|
@ -2,8 +2,8 @@ use crate::{
|
|||
location::{LocationProvider, Url},
|
||||
matching::Routes,
|
||||
params::ParamsMap,
|
||||
ChooseView, MatchInterface, MatchNestedRoutes, MatchParams, Method,
|
||||
PathSegment, RouteList, RouteListing, RouteMatchId,
|
||||
ChooseView, MatchInterface, MatchNestedRoutes, MatchParams, PathSegment,
|
||||
RouteList, RouteListing, RouteMatchId,
|
||||
};
|
||||
use any_spawner::Executor;
|
||||
use either_of::{Either, EitherOf3};
|
||||
|
@ -511,10 +511,8 @@ where
|
|||
RouteListing::new(
|
||||
path,
|
||||
data.ssr_mode,
|
||||
// TODO methods
|
||||
[Method::Get],
|
||||
// TODO static data
|
||||
None,
|
||||
data.methods,
|
||||
data.regenerate,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
|
|
@ -1,9 +1,17 @@
|
|||
use crate::{
|
||||
matching::PathSegment, Method, SsrMode, StaticDataMap, StaticMode,
|
||||
matching::PathSegment,
|
||||
static_routes::{
|
||||
RegenerationFn, ResolvedStaticPath, StaticPath, StaticRoute,
|
||||
},
|
||||
Method, SsrMode,
|
||||
};
|
||||
use futures::future::join_all;
|
||||
use reactive_graph::owner::Owner;
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
collections::HashSet,
|
||||
future::Future,
|
||||
mem,
|
||||
};
|
||||
use tachys::{renderer::Renderer, view::RenderHtml};
|
||||
|
||||
|
@ -13,7 +21,7 @@ pub struct RouteListing {
|
|||
path: Vec<PathSegment>,
|
||||
mode: SsrMode,
|
||||
methods: HashSet<Method>,
|
||||
static_mode: Option<(StaticMode, StaticDataMap)>,
|
||||
regenerate: Vec<RegenerationFn>,
|
||||
}
|
||||
|
||||
impl RouteListing {
|
||||
|
@ -22,19 +30,19 @@ impl RouteListing {
|
|||
path: impl IntoIterator<Item = PathSegment>,
|
||||
mode: SsrMode,
|
||||
methods: impl IntoIterator<Item = Method>,
|
||||
static_mode: Option<(StaticMode, StaticDataMap)>,
|
||||
regenerate: impl IntoIterator<Item = RegenerationFn>,
|
||||
) -> Self {
|
||||
Self {
|
||||
path: path.into_iter().collect(),
|
||||
mode,
|
||||
methods: methods.into_iter().collect(),
|
||||
static_mode,
|
||||
regenerate: regenerate.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a route listing from a path, with the other fields set to default values.
|
||||
pub fn from_path(path: impl IntoIterator<Item = PathSegment>) -> Self {
|
||||
Self::new(path, SsrMode::Async, [], None)
|
||||
Self::new(path, SsrMode::Async, [], [])
|
||||
}
|
||||
|
||||
/// The path this route handles.
|
||||
|
@ -43,8 +51,8 @@ impl RouteListing {
|
|||
}
|
||||
|
||||
/// The rendering mode for this path.
|
||||
pub fn mode(&self) -> SsrMode {
|
||||
self.mode
|
||||
pub fn mode(&self) -> &SsrMode {
|
||||
&self.mode
|
||||
}
|
||||
|
||||
/// The HTTP request methods this path can handle.
|
||||
|
@ -52,56 +60,95 @@ impl RouteListing {
|
|||
self.methods.iter().copied()
|
||||
}
|
||||
|
||||
/// Whether this route is statically rendered.
|
||||
#[inline(always)]
|
||||
pub fn static_mode(&self) -> Option<StaticMode> {
|
||||
self.static_mode.as_ref().map(|n| n.0)
|
||||
/// The set of regeneration functions that should be applied to this route, if it is statically
|
||||
/// generated (either up front or incrementally).
|
||||
pub fn regenerate(&self) -> &[RegenerationFn] {
|
||||
&self.regenerate
|
||||
}
|
||||
|
||||
/// Whether this route is statically rendered.
|
||||
#[inline(always)]
|
||||
pub fn static_data_map(&self) -> Option<&StaticDataMap> {
|
||||
self.static_mode.as_ref().map(|n| &n.1)
|
||||
pub fn static_route(&self) -> Option<&StaticRoute> {
|
||||
match self.mode {
|
||||
SsrMode::Static(ref route) => Some(route),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_static_parts(self) -> Option<(StaticMode, StaticDataMap)> {
|
||||
self.static_mode
|
||||
pub async fn into_static_paths(self) -> Option<Vec<ResolvedStaticPath>> {
|
||||
let params = self.static_route()?.to_prerendered_params().await;
|
||||
Some(StaticPath::new(self.path).into_paths(params))
|
||||
}
|
||||
|
||||
pub async fn generate_static_files<Fut, WriterFut>(
|
||||
mut self,
|
||||
render_fn: impl Fn(&ResolvedStaticPath) -> Fut + Send + Clone + 'static,
|
||||
writer: impl Fn(&ResolvedStaticPath, &Owner, String) -> WriterFut
|
||||
+ Send
|
||||
+ Clone
|
||||
+ 'static,
|
||||
was_404: impl Fn(&Owner) -> bool + Send + Clone + 'static,
|
||||
) where
|
||||
Fut: Future<Output = (Owner, String)> + Send + 'static,
|
||||
WriterFut: Future<Output = Result<(), std::io::Error>> + Send + 'static,
|
||||
{
|
||||
if let SsrMode::Static(_) = self.mode() {
|
||||
let (all_initial_tx, all_initial_rx) = std::sync::mpsc::channel();
|
||||
|
||||
let render_fn = render_fn.clone();
|
||||
let regenerate = mem::take(&mut self.regenerate);
|
||||
let paths = self.into_static_paths().await.unwrap_or_default();
|
||||
|
||||
for path in paths {
|
||||
// Err(_) here would just mean they've dropped the rx and are no longer awaiting
|
||||
// it; we're only using it to notify them it's done so it doesn't matter in that
|
||||
// case
|
||||
_ = all_initial_tx.send(path.build(
|
||||
render_fn.clone(),
|
||||
writer.clone(),
|
||||
was_404.clone(),
|
||||
regenerate.clone(),
|
||||
));
|
||||
}
|
||||
|
||||
join_all(all_initial_rx.try_iter()).await;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
/// 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?;
|
||||
/// 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.mode {
|
||||
SsrMode::Static(route) => {
|
||||
let mut path = StaticPath::new(self.path.clone());
|
||||
for path in path.into_paths(params) {
|
||||
/*path.write(
|
||||
options,
|
||||
app_fn.clone(),
|
||||
additional_context.clone(),
|
||||
)
|
||||
.await?;*/ println!()
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
Ok(true)
|
||||
_ => Ok(false),
|
||||
}
|
||||
}
|
||||
}*/
|
||||
*/
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct RouteList(Vec<RouteListing>);
|
||||
|
||||
impl From<Vec<RouteListing>> for RouteList {
|
||||
|
@ -124,6 +171,45 @@ impl RouteList {
|
|||
pub fn into_inner(self) -> Vec<RouteListing> {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &RouteListing> {
|
||||
self.0.iter()
|
||||
}
|
||||
|
||||
pub async fn into_static_paths(self) -> Vec<ResolvedStaticPath> {
|
||||
futures::future::join_all(
|
||||
self.into_inner()
|
||||
.into_iter()
|
||||
.map(|route_listing| route_listing.into_static_paths()),
|
||||
)
|
||||
.await
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
pub async fn generate_static_files<Fut, WriterFut>(
|
||||
self,
|
||||
render_fn: impl Fn(&ResolvedStaticPath) -> Fut + Send + Clone + 'static,
|
||||
writer: impl Fn(&ResolvedStaticPath, &Owner, String) -> WriterFut
|
||||
+ Send
|
||||
+ Clone
|
||||
+ 'static,
|
||||
was_404: impl Fn(&Owner) -> bool + Send + Clone + 'static,
|
||||
) where
|
||||
Fut: Future<Output = (Owner, String)> + Send + 'static,
|
||||
WriterFut: Future<Output = Result<(), std::io::Error>> + Send + 'static,
|
||||
{
|
||||
join_all(self.into_inner().into_iter().map(|route| {
|
||||
route.generate_static_files(
|
||||
render_fn.clone(),
|
||||
writer.clone(),
|
||||
was_404.clone(),
|
||||
)
|
||||
}))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
impl RouteList {
|
||||
|
|
|
@ -155,8 +155,10 @@ pub fn use_location() -> Location {
|
|||
location
|
||||
}
|
||||
|
||||
pub(crate) type RawParamsMap = ArcMemo<ParamsMap>;
|
||||
|
||||
#[track_caller]
|
||||
fn use_params_raw() -> ArcMemo<ParamsMap> {
|
||||
fn use_params_raw() -> RawParamsMap {
|
||||
use_context().expect(
|
||||
"Tried to access params outside the context of a matched <Route>.",
|
||||
)
|
||||
|
|
|
@ -16,7 +16,7 @@ pub mod nested_router;
|
|||
pub mod params;
|
||||
//mod router;
|
||||
mod ssr_mode;
|
||||
mod static_route;
|
||||
pub mod static_routes;
|
||||
|
||||
pub use generate_route_list::*;
|
||||
#[doc(inline)]
|
||||
|
@ -26,4 +26,3 @@ pub use method::*;
|
|||
pub use navigate::*;
|
||||
//pub use router::*;
|
||||
pub use ssr_mode::*;
|
||||
pub use static_route::*;
|
||||
|
|
|
@ -201,10 +201,29 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub(crate) fn unescape(s: &str) -> String {
|
||||
percent_encoding::percent_decode_str(s)
|
||||
.decode_utf8()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub(crate) fn unescape(s: &str) -> String {
|
||||
js_sys::decode_uri_component(s).unwrap().into()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub(crate) fn unescape_minimal(s: &str) -> String {
|
||||
js_sys::decode_uri(s).unwrap().into()
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub(crate) fn unescape_minimal(s: &str) -> String {
|
||||
unescape(s)
|
||||
}
|
||||
|
||||
pub(crate) fn handle_anchor_click<NavFn, NavFut>(
|
||||
router_base: Option<Cow<'static, str>>,
|
||||
parse_with_base: fn(&str, &str) -> Result<Url, JsValue>,
|
||||
|
@ -259,7 +278,7 @@ where
|
|||
}
|
||||
|
||||
let url = parse_with_base(href.as_str(), &origin).unwrap();
|
||||
let path_name = unescape(&url.path);
|
||||
let path_name = unescape_minimal(&url.path);
|
||||
|
||||
// let browser handle this event if it leaves our domain
|
||||
// or our base path
|
||||
|
|
|
@ -6,10 +6,10 @@ pub use path_segment::*;
|
|||
mod horizontal;
|
||||
mod nested;
|
||||
mod vertical;
|
||||
use crate::SsrMode;
|
||||
use crate::{static_routes::RegenerationFn, Method, SsrMode};
|
||||
pub use horizontal::*;
|
||||
pub use nested::*;
|
||||
use std::{borrow::Cow, marker::PhantomData};
|
||||
use std::{borrow::Cow, collections::HashSet, marker::PhantomData};
|
||||
use tachys::{
|
||||
renderer::Renderer,
|
||||
view::{Render, RenderHtml},
|
||||
|
@ -145,6 +145,8 @@ where
|
|||
pub struct GeneratedRouteData {
|
||||
pub segments: Vec<PathSegment>,
|
||||
pub ssr_mode: SsrMode,
|
||||
pub methods: HashSet<Method>,
|
||||
pub regenerate: Vec<RegenerationFn>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -2,11 +2,12 @@ use super::{
|
|||
MatchInterface, MatchNestedRoutes, PartialPathMatch, PathSegment,
|
||||
PossibleRouteMatch, RouteMatchId,
|
||||
};
|
||||
use crate::{ChooseView, GeneratedRouteData, MatchParams, SsrMode};
|
||||
use crate::{ChooseView, GeneratedRouteData, MatchParams, Method, SsrMode};
|
||||
use core::{fmt, iter};
|
||||
use either_of::Either;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::HashSet,
|
||||
marker::PhantomData,
|
||||
sync::atomic::{AtomicU16, Ordering},
|
||||
};
|
||||
|
@ -19,7 +20,7 @@ mod tuples;
|
|||
|
||||
static ROUTE_ID: AtomicU16 = AtomicU16::new(1);
|
||||
|
||||
#[derive(Debug, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct NestedRoute<Segments, Children, Data, View, R> {
|
||||
id: u16,
|
||||
segments: Segments,
|
||||
|
@ -27,6 +28,7 @@ pub struct NestedRoute<Segments, Children, Data, View, R> {
|
|||
data: Data,
|
||||
view: View,
|
||||
rndr: PhantomData<R>,
|
||||
methods: HashSet<Method>,
|
||||
ssr_mode: SsrMode,
|
||||
}
|
||||
|
||||
|
@ -46,7 +48,8 @@ where
|
|||
data: self.data.clone(),
|
||||
view: self.view.clone(),
|
||||
rndr: PhantomData,
|
||||
ssr_mode: self.ssr_mode,
|
||||
methods: self.methods.clone(),
|
||||
ssr_mode: self.ssr_mode.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -64,6 +67,7 @@ impl<Segments, View, R> NestedRoute<Segments, (), (), View, R> {
|
|||
data: (),
|
||||
view,
|
||||
rndr: PhantomData,
|
||||
methods: [Method::Get].into(),
|
||||
ssr_mode: Default::default(),
|
||||
}
|
||||
}
|
||||
|
@ -81,6 +85,7 @@ impl<Segments, Data, View, R> NestedRoute<Segments, (), Data, View, R> {
|
|||
view,
|
||||
rndr,
|
||||
ssr_mode,
|
||||
methods,
|
||||
..
|
||||
} = self;
|
||||
NestedRoute {
|
||||
|
@ -90,6 +95,7 @@ impl<Segments, Data, View, R> NestedRoute<Segments, (), Data, View, R> {
|
|||
data,
|
||||
view,
|
||||
ssr_mode,
|
||||
methods,
|
||||
rndr,
|
||||
}
|
||||
}
|
||||
|
@ -249,25 +255,44 @@ where
|
|||
let mut segment_routes = Vec::new();
|
||||
self.segments.generate_path(&mut segment_routes);
|
||||
let children = self.children.as_ref();
|
||||
let ssr_mode = self.ssr_mode;
|
||||
let ssr_mode = self.ssr_mode.clone();
|
||||
let methods = self.methods.clone();
|
||||
let regenerate = match &ssr_mode {
|
||||
SsrMode::Static(data) => match data.regenerate.as_ref() {
|
||||
None => vec![],
|
||||
Some(regenerate) => vec![regenerate.clone()]
|
||||
}
|
||||
_ => vec![]
|
||||
};
|
||||
|
||||
match children {
|
||||
None => Either::Left(iter::once(GeneratedRouteData {
|
||||
segments: segment_routes,
|
||||
ssr_mode
|
||||
ssr_mode,
|
||||
methods,
|
||||
regenerate
|
||||
})),
|
||||
Some(children) => {
|
||||
Either::Right(children.generate_routes().into_iter().map(move |child| {
|
||||
// extend this route's segments with child segments
|
||||
let segments = segment_routes.clone().into_iter().chain(child.segments).collect();
|
||||
|
||||
let mut methods = methods.clone();
|
||||
methods.extend(child.methods);
|
||||
|
||||
let mut regenerate = regenerate.clone();
|
||||
regenerate.extend(child.regenerate);
|
||||
|
||||
if child.ssr_mode > ssr_mode {
|
||||
GeneratedRouteData {
|
||||
segments,
|
||||
ssr_mode: child.ssr_mode,
|
||||
methods, regenerate
|
||||
}
|
||||
} else {
|
||||
GeneratedRouteData {
|
||||
segments,
|
||||
ssr_mode,
|
||||
ssr_mode: ssr_mode.clone(), methods, regenerate
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
|
|
@ -3,8 +3,8 @@ use crate::{
|
|||
location::{LocationProvider, Url},
|
||||
matching::Routes,
|
||||
params::ParamsMap,
|
||||
ChooseView, MatchInterface, MatchNestedRoutes, MatchParams, Method,
|
||||
PathSegment, RouteList, RouteListing, RouteMatchId,
|
||||
ChooseView, MatchInterface, MatchNestedRoutes, MatchParams, PathSegment,
|
||||
RouteList, RouteListing, RouteMatchId,
|
||||
};
|
||||
use any_spawner::Executor;
|
||||
use either_of::{Either, EitherOf3};
|
||||
|
@ -272,10 +272,8 @@ where
|
|||
RouteListing::new(
|
||||
path,
|
||||
data.ssr_mode,
|
||||
// TODO methods
|
||||
[Method::Get],
|
||||
// TODO static data
|
||||
None,
|
||||
data.methods,
|
||||
data.regenerate,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use crate::static_routes::StaticRoute;
|
||||
|
||||
/// Indicates which rendering mode should be used for this route during server-side rendering.
|
||||
///
|
||||
/// Leptos supports the following ways of rendering HTML that contains `async` data loaded
|
||||
|
@ -18,15 +20,17 @@
|
|||
/// 5. **`Async`**: Load all resources on the server. Wait until all data are loaded, and render HTML in one sweep.
|
||||
/// - *Pros*: Better handling for meta tags (because you know async data even before you render the `<head>`). Faster complete load than **synchronous** because async resources begin loading on server.
|
||||
/// - *Cons*: Slower load time/TTFB: you need to wait for all async resources to load before displaying anything on the client.
|
||||
/// 6. **`Static`**:
|
||||
///
|
||||
/// The mode defaults to out-of-order streaming. For a path that includes multiple nested routes, the most
|
||||
/// restrictive mode will be used: i.e., if even a single nested route asks for `Async` rendering, the whole initial
|
||||
/// request will be rendered `Async`. (`Async` is the most restricted requirement, followed by `InOrder`, `PartiallyBlocked`, and `OutOfOrder`.)
|
||||
#[derive(Default, Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
#[derive(Default, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum SsrMode {
|
||||
#[default]
|
||||
OutOfOrder,
|
||||
PartiallyBlocked,
|
||||
InOrder,
|
||||
Async,
|
||||
Static(StaticRoute),
|
||||
}
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
/// 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,
|
||||
}
|
||||
|
||||
// TODO
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StaticDataMap;
|
||||
|
||||
impl StaticDataMap {
|
||||
#[allow(clippy::new_without_default)] // TODO
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
363
router/src/static_routes.rs
Normal file
363
router/src/static_routes.rs
Normal file
|
@ -0,0 +1,363 @@
|
|||
use crate::{hooks::RawParamsMap, params::ParamsMap, PathSegment};
|
||||
use futures::{channel::oneshot, stream, Stream, StreamExt};
|
||||
use leptos::spawn::spawn;
|
||||
use reactive_graph::{owner::Owner, traits::GetUntracked};
|
||||
use std::{
|
||||
fmt::{Debug, Display},
|
||||
future::Future,
|
||||
ops::Deref,
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
type PinnedFuture<T> = Pin<Box<dyn Future<Output = T> + Send>>;
|
||||
type PinnedStream<T> = Pin<Box<dyn Stream<Item = T> + Send>>;
|
||||
|
||||
pub type StaticParams = Arc<StaticParamsFn>;
|
||||
pub type StaticParamsFn =
|
||||
dyn Fn() -> PinnedFuture<StaticParamsMap> + Send + Sync + 'static;
|
||||
|
||||
#[derive(Clone)]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub struct RegenerationFn(
|
||||
Arc<dyn Fn(&ParamsMap) -> PinnedStream<()> + Send + Sync>,
|
||||
);
|
||||
|
||||
impl Debug for RegenerationFn {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("RegenerationFn").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for RegenerationFn {
|
||||
type Target = dyn Fn(&ParamsMap) -> PinnedStream<()> + Send + Sync;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&*self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for RegenerationFn {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
Arc::ptr_eq(&self.0, &other.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct StaticRoute {
|
||||
pub(crate) prerender_params: Option<StaticParams>,
|
||||
pub(crate) regenerate: Option<RegenerationFn>,
|
||||
}
|
||||
|
||||
impl StaticRoute {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn prerender_params<Fut>(
|
||||
mut self,
|
||||
params: impl Fn() -> Fut + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
Fut: Future<Output = StaticParamsMap> + Send + 'static,
|
||||
{
|
||||
self.prerender_params = Some(Arc::new(move || Box::pin(params())));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn regenerate<St>(
|
||||
mut self,
|
||||
invalidate: impl Fn(&ParamsMap) -> St + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
St: Stream<Item = ()> + Send + 'static,
|
||||
{
|
||||
self.regenerate = Some(RegenerationFn(Arc::new(move |params| {
|
||||
Box::pin(invalidate(params))
|
||||
})));
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn to_prerendered_params(&self) -> Option<StaticParamsMap> {
|
||||
match &self.prerender_params {
|
||||
None => None,
|
||||
Some(params) => Some(params().await),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for StaticRoute {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("StaticRoute").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for StaticRoute {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for StaticRoute {
|
||||
fn cmp(&self, _other: &Self) -> std::cmp::Ordering {
|
||||
std::cmp::Ordering::Equal
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for StaticRoute {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
let prerender = match (&self.prerender_params, &other.prerender_params)
|
||||
{
|
||||
(None, None) => true,
|
||||
(None, Some(_)) | (Some(_), None) => false,
|
||||
(Some(this), Some(that)) => Arc::ptr_eq(this, that),
|
||||
};
|
||||
prerender && (self.regenerate == other.regenerate)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for StaticRoute {}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct StaticParamsMap(pub Vec<(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>) {
|
||||
let key = key.to_string();
|
||||
for item in self.0.iter_mut() {
|
||||
if item.0 == key {
|
||||
item.1 = value;
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.0.push((key, value));
|
||||
}
|
||||
|
||||
/// Get a value from the map.
|
||||
#[inline]
|
||||
pub fn get(&self, key: &str) -> Option<&Vec<String>> {
|
||||
self.0
|
||||
.iter()
|
||||
.find_map(|entry| (entry.0 == key).then_some(&entry.1))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for StaticParamsMap {
|
||||
type Item = (String, Vec<String>);
|
||||
type IntoIter = StaticParamsIter;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
StaticParamsIter(self.0.into_iter())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct StaticParamsIter(
|
||||
<Vec<(String, Vec<String>)> as IntoIterator>::IntoIter,
|
||||
);
|
||||
|
||||
impl Iterator for StaticParamsIter {
|
||||
type Item = (String, Vec<String>);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.0.next()
|
||||
}
|
||||
}
|
||||
|
||||
impl<A> FromIterator<A> for StaticParamsMap
|
||||
where
|
||||
A: Into<(String, Vec<String>)>,
|
||||
{
|
||||
fn from_iter<T: IntoIterator<Item = A>>(iter: T) -> Self {
|
||||
Self(iter.into_iter().map(Into::into).collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[derive(Debug)]
|
||||
pub struct StaticPath {
|
||||
segments: Vec<PathSegment>,
|
||||
}
|
||||
|
||||
impl StaticPath {
|
||||
pub fn new(segments: Vec<PathSegment>) -> StaticPath {
|
||||
Self { segments }
|
||||
}
|
||||
|
||||
pub fn into_paths(
|
||||
self,
|
||||
params: Option<StaticParamsMap>,
|
||||
) -> Vec<ResolvedStaticPath> {
|
||||
use PathSegment::*;
|
||||
let mut paths = vec![ResolvedStaticPath {
|
||||
path: String::new(),
|
||||
}];
|
||||
|
||||
for segment in &self.segments {
|
||||
match segment {
|
||||
Unit => {}
|
||||
Static(s) => {
|
||||
paths = paths
|
||||
.into_iter()
|
||||
.map(|p| {
|
||||
if s.starts_with("/") {
|
||||
ResolvedStaticPath {
|
||||
path: format!("{}{s}", p.path),
|
||||
}
|
||||
} else {
|
||||
ResolvedStaticPath {
|
||||
path: format!("{}/{s}", p.path),
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
}
|
||||
Param(name) | Splat(name) => {
|
||||
let mut new_paths = vec![];
|
||||
if let Some(params) = params.as_ref() {
|
||||
for path in paths {
|
||||
if let Some(params) = params.get(name) {
|
||||
for val in params.iter() {
|
||||
new_paths.push(if val.starts_with("/") {
|
||||
ResolvedStaticPath {
|
||||
path: format!(
|
||||
"{}{}",
|
||||
path.path, val
|
||||
),
|
||||
}
|
||||
} else {
|
||||
ResolvedStaticPath {
|
||||
path: format!(
|
||||
"{}/{}",
|
||||
path.path, val
|
||||
),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
paths = new_paths;
|
||||
}
|
||||
}
|
||||
}
|
||||
paths
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ResolvedStaticPath {
|
||||
pub(crate) path: String,
|
||||
}
|
||||
|
||||
impl ResolvedStaticPath {
|
||||
pub fn new(path: impl Into<String>) -> Self {
|
||||
Self { path: path.into() }
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for ResolvedStaticPath {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.path.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ResolvedStaticPath {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
Display::fmt(&self.path, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl ResolvedStaticPath {
|
||||
pub async fn build<Fut, WriterFut>(
|
||||
self,
|
||||
render_fn: impl Fn(&ResolvedStaticPath) -> Fut + Send + Clone + 'static,
|
||||
writer: impl Fn(&ResolvedStaticPath, &Owner, String) -> WriterFut
|
||||
+ Send
|
||||
+ Clone
|
||||
+ 'static,
|
||||
was_404: impl Fn(&Owner) -> bool + Send + Clone + 'static,
|
||||
regenerate: Vec<RegenerationFn>,
|
||||
) -> (Owner, Option<String>)
|
||||
where
|
||||
Fut: Future<Output = (Owner, String)> + Send + 'static,
|
||||
WriterFut: Future<Output = Result<(), std::io::Error>> + Send + 'static,
|
||||
{
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
// spawns a separate task for each path it's rendering
|
||||
// this allows us to parallelize all static site rendering,
|
||||
// and also to create long-lived tasks
|
||||
spawn({
|
||||
let render_fn = render_fn.clone();
|
||||
let writer = writer.clone();
|
||||
let was_error = was_404.clone();
|
||||
async move {
|
||||
// render and write the initial page
|
||||
let (owner, html) = render_fn(&self).await;
|
||||
|
||||
// if rendering this page resulted in an error (404, 500, etc.)
|
||||
// then we should not cache it: the `was_error` function can handle notifying
|
||||
// the user that there was an error, and the server can give a dynamic response
|
||||
// that will include the 404 or 500
|
||||
if was_error(&owner) {
|
||||
// can ignore errors from channel here, because it just means we're not
|
||||
// awaiting the Future
|
||||
_ = tx.send((owner.clone(), Some(html)));
|
||||
} else {
|
||||
_ = tx.send((owner.clone(), None));
|
||||
if let Err(e) = writer(&self, &owner, html).await {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::warn!("{e}");
|
||||
|
||||
#[cfg(not(feature = "tracing"))]
|
||||
eprintln!("{e}");
|
||||
}
|
||||
}
|
||||
|
||||
// if there's a regeneration function, keep looping
|
||||
let params = if regenerate.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
owner
|
||||
.use_context_bidirectional::<RawParamsMap>()
|
||||
.expect(
|
||||
"using static routing, but couldn't find \
|
||||
ParamsMap",
|
||||
)
|
||||
.get_untracked(),
|
||||
)
|
||||
};
|
||||
let mut regenerate = stream::select_all(
|
||||
regenerate
|
||||
.into_iter()
|
||||
.map(|r| owner.with(|| r(params.as_ref().unwrap()))),
|
||||
);
|
||||
while regenerate.next().await.is_some() {
|
||||
let (owner, html) = render_fn(&self).await;
|
||||
if !was_error(&owner) {
|
||||
if let Err(e) = writer(&self, &owner, html).await {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::warn!("{e}");
|
||||
|
||||
#[cfg(not(feature = "tracing"))]
|
||||
eprintln!("{e}");
|
||||
}
|
||||
}
|
||||
drop(owner);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
rx.await.unwrap()
|
||||
}
|
||||
}
|
|
@ -75,6 +75,9 @@ impl SegmentParser {
|
|||
lit.trim_start_matches(['"', '/'])
|
||||
.trim_end_matches(['"', '/']),
|
||||
);
|
||||
if lit.ends_with(r#"/""#) && lit != r#""/""# {
|
||||
self.segments.push(Segment::Static("/".to_string()));
|
||||
}
|
||||
}
|
||||
TokenTree::Group(_) => unimplemented!(),
|
||||
TokenTree::Ident(_) => unimplemented!(),
|
||||
|
@ -102,13 +105,14 @@ impl SegmentParser {
|
|||
|
||||
impl Segment {
|
||||
fn is_valid(segment: &str) -> bool {
|
||||
segment.chars().all(|c| {
|
||||
c.is_ascii_digit()
|
||||
|| c.is_ascii_lowercase()
|
||||
|| c.is_ascii_uppercase()
|
||||
|| RFC3986_UNRESERVED.contains(&c)
|
||||
|| RFC3986_PCHAR_OTHER.contains(&c)
|
||||
})
|
||||
segment == "/"
|
||||
|| segment.chars().all(|c| {
|
||||
c.is_ascii_digit()
|
||||
|| c.is_ascii_lowercase()
|
||||
|| c.is_ascii_uppercase()
|
||||
|| RFC3986_UNRESERVED.contains(&c)
|
||||
|| RFC3986_PCHAR_OTHER.contains(&c)
|
||||
})
|
||||
}
|
||||
|
||||
fn ensure_valid(&self) {
|
||||
|
|
|
@ -64,14 +64,14 @@ fn parses_no_slashes() {
|
|||
|
||||
#[test]
|
||||
fn parses_no_leading_slash() {
|
||||
let output = path!("home/");
|
||||
let output = path!("home");
|
||||
assert_eq!(output, (StaticSegment("home"),));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_trailing_slash() {
|
||||
let output = path!("/home/");
|
||||
assert_eq!(output, (StaticSegment("home"),));
|
||||
assert_eq!(output, (StaticSegment("home"), StaticSegment("/")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -105,6 +105,19 @@ fn parses_mixed_segment_types() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_trailing_slash_after_param() {
|
||||
let output = path!("/foo/:bar/");
|
||||
assert_eq!(
|
||||
output,
|
||||
(
|
||||
StaticSegment("foo"),
|
||||
ParamSegment("bar"),
|
||||
StaticSegment("/")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_consecutive_static() {
|
||||
let output = path!("/foo/bar/baz");
|
||||
|
|
Loading…
Reference in a new issue