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]
|
[dependencies]
|
||||||
actix-http = "3.8"
|
actix-http = "3.8"
|
||||||
|
actix-files = "0.6"
|
||||||
actix-web = "4.8"
|
actix-web = "4.8"
|
||||||
futures = "0.3.30"
|
futures = "0.3.30"
|
||||||
any_spawner = { workspace = true, features = ["tokio"] }
|
any_spawner = { workspace = true, features = ["tokio"] }
|
||||||
|
@ -25,6 +26,8 @@ parking_lot = "0.12.3"
|
||||||
tracing = { version = "0.1", optional = true }
|
tracing = { version = "0.1", optional = true }
|
||||||
tokio = { version = "1.39", features = ["rt", "fs"] }
|
tokio = { version = "1.39", features = ["rt", "fs"] }
|
||||||
send_wrapper = "0.6.0"
|
send_wrapper = "0.6.0"
|
||||||
|
dashmap = "6"
|
||||||
|
once_cell = "1"
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
rustdoc-args = ["--generate-link-to-definition"]
|
rustdoc-args = ["--generate-link-to-definition"]
|
||||||
|
|
|
@ -6,30 +6,38 @@
|
||||||
//! [`examples`](https://github.com/leptos-rs/leptos/tree/main/examples)
|
//! [`examples`](https://github.com/leptos-rs/leptos/tree/main/examples)
|
||||||
//! directory in the Leptos repository.
|
//! directory in the Leptos repository.
|
||||||
|
|
||||||
|
use actix_files::NamedFile;
|
||||||
use actix_http::header::{HeaderName, HeaderValue, ACCEPT, LOCATION, REFERER};
|
use actix_http::header::{HeaderName, HeaderValue, ACCEPT, LOCATION, REFERER};
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
body::BoxBody,
|
body::BoxBody,
|
||||||
dev::{ServiceFactory, ServiceRequest},
|
dev::{ServiceFactory, ServiceRequest},
|
||||||
http::header,
|
http::header,
|
||||||
web::{Payload, ServiceConfig},
|
test,
|
||||||
|
web::{Data, Payload, ServiceConfig},
|
||||||
*,
|
*,
|
||||||
};
|
};
|
||||||
|
use dashmap::DashMap;
|
||||||
use futures::{stream::once, Stream, StreamExt};
|
use futures::{stream::once, Stream, StreamExt};
|
||||||
use http::StatusCode;
|
use http::StatusCode;
|
||||||
use hydration_context::SsrSharedContext;
|
use hydration_context::SsrSharedContext;
|
||||||
use leptos::{
|
use leptos::{
|
||||||
|
config::LeptosOptions,
|
||||||
context::{provide_context, use_context},
|
context::{provide_context, use_context},
|
||||||
|
prelude::expect_context,
|
||||||
reactive_graph::{computed::ScopedFuture, owner::Owner},
|
reactive_graph::{computed::ScopedFuture, owner::Owner},
|
||||||
IntoView, *,
|
IntoView,
|
||||||
};
|
};
|
||||||
use leptos_integration_utils::{
|
use leptos_integration_utils::{
|
||||||
BoxedFnOnce, ExtendResponse, PinnedFuture, PinnedStream,
|
BoxedFnOnce, ExtendResponse, PinnedFuture, PinnedStream,
|
||||||
};
|
};
|
||||||
use leptos_meta::ServerMetaContext;
|
use leptos_meta::ServerMetaContext;
|
||||||
use leptos_router::{
|
use leptos_router::{
|
||||||
components::provide_server_redirect, location::RequestUrl, PathSegment,
|
components::provide_server_redirect,
|
||||||
RouteList, RouteListing, SsrMode, StaticDataMap, StaticMode, *,
|
location::RequestUrl,
|
||||||
|
static_routes::{RegenerationFn, ResolvedStaticPath},
|
||||||
|
Method, PathSegment, RouteList, RouteListing, SsrMode,
|
||||||
};
|
};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use send_wrapper::SendWrapper;
|
use send_wrapper::SendWrapper;
|
||||||
use server_fn::{
|
use server_fn::{
|
||||||
|
@ -37,7 +45,9 @@ use server_fn::{
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
fmt::{Debug, Display},
|
fmt::{Debug, Display},
|
||||||
|
future::Future,
|
||||||
ops::{Deref, DerefMut},
|
ops::{Deref, DerefMut},
|
||||||
|
path::Path,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -728,13 +738,25 @@ pub fn render_app_async_with_context<IV>(
|
||||||
where
|
where
|
||||||
IV: IntoView + 'static,
|
IV: IntoView + 'static,
|
||||||
{
|
{
|
||||||
handle_response(method, additional_context, app_fn, |app, chunks| {
|
handle_response(method, additional_context, app_fn, async_stream_builder)
|
||||||
Box::pin(async move {
|
}
|
||||||
let app = app.to_html_stream_in_order().collect::<String>().await;
|
|
||||||
let chunks = chunks();
|
fn async_stream_builder<IV>(
|
||||||
Box::pin(once(async move { app }).chain(chunks))
|
app: IV,
|
||||||
as PinnedStream<String>
|
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
|
/// 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.
|
/// 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>(
|
pub fn generate_route_list<IV>(
|
||||||
app_fn: impl Fn() -> IV + 'static + Clone,
|
app_fn: impl Fn() -> IV + 'static + Send + Clone,
|
||||||
) -> Vec<ActixRouteListing>
|
) -> Vec<ActixRouteListing>
|
||||||
where
|
where
|
||||||
IV: IntoView + 'static,
|
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
|
/// 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.
|
/// 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>(
|
pub fn generate_route_list_with_ssg<IV>(
|
||||||
app_fn: impl Fn() -> IV + 'static + Clone,
|
app_fn: impl Fn() -> IV + 'static + Send + Clone,
|
||||||
) -> (Vec<ActixRouteListing>, StaticDataMap)
|
) -> (Vec<ActixRouteListing>, StaticRouteGenerator)
|
||||||
where
|
where
|
||||||
IV: IntoView + 'static,
|
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
|
/// 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
|
/// 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>(
|
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>>,
|
excluded_routes: Option<Vec<String>>,
|
||||||
) -> Vec<ActixRouteListing>
|
) -> Vec<ActixRouteListing>
|
||||||
where
|
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
|
/// 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
|
/// 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>(
|
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>>,
|
excluded_routes: Option<Vec<String>>,
|
||||||
) -> (Vec<ActixRouteListing>, StaticDataMap)
|
) -> (Vec<ActixRouteListing>, StaticRouteGenerator)
|
||||||
where
|
where
|
||||||
IV: IntoView + 'static,
|
IV: IntoView + 'static,
|
||||||
{
|
{
|
||||||
|
@ -912,7 +934,7 @@ pub struct ActixRouteListing {
|
||||||
path: String,
|
path: String,
|
||||||
mode: SsrMode,
|
mode: SsrMode,
|
||||||
methods: Vec<leptos_router::Method>,
|
methods: Vec<leptos_router::Method>,
|
||||||
static_mode: Option<(StaticMode, StaticDataMap)>,
|
regenerate: Vec<RegenerationFn>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<RouteListing> for ActixRouteListing {
|
impl From<RouteListing> for ActixRouteListing {
|
||||||
|
@ -925,12 +947,12 @@ impl From<RouteListing> for ActixRouteListing {
|
||||||
};
|
};
|
||||||
let mode = value.mode();
|
let mode = value.mode();
|
||||||
let methods = value.methods().collect();
|
let methods = value.methods().collect();
|
||||||
let static_mode = value.into_static_parts();
|
let regenerate = value.regenerate().into();
|
||||||
Self {
|
Self {
|
||||||
path,
|
path,
|
||||||
mode,
|
mode: mode.clone(),
|
||||||
methods,
|
methods,
|
||||||
static_mode,
|
regenerate,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -941,13 +963,13 @@ impl ActixRouteListing {
|
||||||
path: String,
|
path: String,
|
||||||
mode: SsrMode,
|
mode: SsrMode,
|
||||||
methods: impl IntoIterator<Item = leptos_router::Method>,
|
methods: impl IntoIterator<Item = leptos_router::Method>,
|
||||||
static_mode: Option<(StaticMode, StaticDataMap)>,
|
regenerate: impl Into<Vec<RegenerationFn>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
path,
|
path,
|
||||||
mode,
|
mode,
|
||||||
methods: methods.into_iter().collect(),
|
methods: methods.into_iter().collect(),
|
||||||
static_mode,
|
regenerate: regenerate.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -958,19 +980,13 @@ impl ActixRouteListing {
|
||||||
|
|
||||||
/// The rendering mode for this path.
|
/// The rendering mode for this path.
|
||||||
pub fn mode(&self) -> SsrMode {
|
pub fn mode(&self) -> SsrMode {
|
||||||
self.mode
|
self.mode.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The HTTP request methods this path can handle.
|
/// The HTTP request methods this path can handle.
|
||||||
pub fn methods(&self) -> impl Iterator<Item = leptos_router::Method> + '_ {
|
pub fn methods(&self) -> impl Iterator<Item = leptos_router::Method> + '_ {
|
||||||
self.methods.iter().copied()
|
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
|
/// 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.
|
/// 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.
|
/// Additional context will be provided to the app Element.
|
||||||
pub fn generate_route_list_with_exclusions_and_ssg_and_context<IV>(
|
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>>,
|
excluded_routes: Option<Vec<String>>,
|
||||||
additional_context: impl Fn() + 'static + Clone,
|
additional_context: impl Fn() + 'static + Send + Clone,
|
||||||
) -> (Vec<ActixRouteListing>, StaticDataMap)
|
) -> (Vec<ActixRouteListing>, StaticRouteGenerator)
|
||||||
where
|
where
|
||||||
IV: IntoView + 'static,
|
IV: IntoView + 'static,
|
||||||
{
|
{
|
||||||
|
@ -1001,6 +1017,12 @@ where
|
||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let generator = StaticRouteGenerator::new(
|
||||||
|
&routes,
|
||||||
|
app_fn.clone(),
|
||||||
|
additional_context.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
// Axum's Router defines Root routes as "/" not ""
|
// Axum's Router defines Root routes as "/" not ""
|
||||||
let mut routes = routes
|
let mut routes = routes
|
||||||
.into_inner()
|
.into_inner()
|
||||||
|
@ -1014,7 +1036,7 @@ where
|
||||||
"/".to_string(),
|
"/".to_string(),
|
||||||
Default::default(),
|
Default::default(),
|
||||||
[leptos_router::Method::Get],
|
[leptos_router::Method::Get],
|
||||||
None,
|
vec![],
|
||||||
)]
|
)]
|
||||||
} else {
|
} else {
|
||||||
// Routes to exclude from auto generation
|
// Routes to exclude from auto generation
|
||||||
|
@ -1024,192 +1046,251 @@ where
|
||||||
}
|
}
|
||||||
routes
|
routes
|
||||||
},
|
},
|
||||||
StaticDataMap::new(), // TODO
|
generator,
|
||||||
//static_data_map,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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> {
|
pub enum DataResponse<T> {
|
||||||
Data(T),
|
Data(T),
|
||||||
Response(actix_web::dev::Response<BoxBody>),
|
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
|
/// 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.
|
/// having to use wildcards or manually define all routes in multiple places.
|
||||||
pub trait LeptosRoutes {
|
pub trait LeptosRoutes {
|
||||||
|
@ -1290,19 +1371,15 @@ where
|
||||||
provide_context(method);
|
provide_context(method);
|
||||||
additional_context();
|
additional_context();
|
||||||
};
|
};
|
||||||
router = if let Some(static_mode) = listing.static_mode() {
|
router = if matches!(listing.mode(), SsrMode::Static(_)) {
|
||||||
_ = static_mode;
|
router.route(
|
||||||
todo!() /*
|
path,
|
||||||
router.route(
|
handle_static_route(
|
||||||
path,
|
additional_context_and_method.clone(),
|
||||||
static_route(
|
app_fn.clone(),
|
||||||
app_fn.clone(),
|
listing.regenerate.clone(),
|
||||||
additional_context_and_method.clone(),
|
),
|
||||||
method,
|
)
|
||||||
static_mode,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
*/
|
|
||||||
} else {
|
} else {
|
||||||
router.route(
|
router.route(
|
||||||
path,
|
path,
|
||||||
|
@ -1334,6 +1411,7 @@ where
|
||||||
app_fn.clone(),
|
app_fn.clone(),
|
||||||
method,
|
method,
|
||||||
),
|
),
|
||||||
|
_ => unreachable!()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
@ -1390,7 +1468,17 @@ impl LeptosRoutes for &mut ServiceConfig {
|
||||||
let mode = listing.mode();
|
let mode = listing.mode();
|
||||||
|
|
||||||
for method in listing.methods() {
|
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,
|
path,
|
||||||
match mode {
|
match mode {
|
||||||
SsrMode::OutOfOrder => {
|
SsrMode::OutOfOrder => {
|
||||||
|
@ -1420,8 +1508,10 @@ impl LeptosRoutes for &mut ServiceConfig {
|
||||||
app_fn.clone(),
|
app_fn.clone(),
|
||||||
method,
|
method,
|
||||||
),
|
),
|
||||||
|
_ => unreachable!()
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ hydration_context = { workspace = true }
|
||||||
axum = { version = "0.7.5", default-features = false, features = [
|
axum = { version = "0.7.5", default-features = false, features = [
|
||||||
"matched-path",
|
"matched-path",
|
||||||
] }
|
] }
|
||||||
|
dashmap = "6"
|
||||||
futures = "0.3.30"
|
futures = "0.3.30"
|
||||||
http = "1.1"
|
http = "1.1"
|
||||||
http-body-util = "0.1.2"
|
http-body-util = "0.1.2"
|
||||||
|
@ -23,6 +24,7 @@ leptos_macro = { workspace = true, features = ["axum"] }
|
||||||
leptos_meta = { workspace = true, features = ["ssr"] }
|
leptos_meta = { workspace = true, features = ["ssr"] }
|
||||||
leptos_router = { workspace = true, features = ["ssr"] }
|
leptos_router = { workspace = true, features = ["ssr"] }
|
||||||
leptos_integration_utils = { workspace = true }
|
leptos_integration_utils = { workspace = true }
|
||||||
|
once_cell = "1"
|
||||||
parking_lot = "0.12.3"
|
parking_lot = "0.12.3"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
tokio = { version = "1.39", default-features = false }
|
tokio = { version = "1.39", default-features = false }
|
||||||
|
|
|
@ -32,9 +32,11 @@
|
||||||
//! [`examples`](https://github.com/leptos-rs/leptos/tree/main/examples)
|
//! [`examples`](https://github.com/leptos-rs/leptos/tree/main/examples)
|
||||||
//! directory in the Leptos repository.
|
//! directory in the Leptos repository.
|
||||||
|
|
||||||
|
#[cfg(feature = "default")]
|
||||||
|
use axum::http::Uri;
|
||||||
use axum::{
|
use axum::{
|
||||||
body::{Body, Bytes},
|
body::{Body, Bytes},
|
||||||
extract::{FromRequestParts, MatchedPath},
|
extract::{FromRef, FromRequestParts, MatchedPath, State},
|
||||||
http::{
|
http::{
|
||||||
header::{self, HeaderName, HeaderValue, ACCEPT, LOCATION, REFERER},
|
header::{self, HeaderName, HeaderValue, ACCEPT, LOCATION, REFERER},
|
||||||
request::Parts,
|
request::Parts,
|
||||||
|
@ -44,10 +46,7 @@ use axum::{
|
||||||
routing::{delete, get, patch, post, put},
|
routing::{delete, get, patch, post, put},
|
||||||
};
|
};
|
||||||
#[cfg(feature = "default")]
|
#[cfg(feature = "default")]
|
||||||
use axum::{
|
use dashmap::DashMap;
|
||||||
extract::{FromRef, State},
|
|
||||||
http::Uri,
|
|
||||||
};
|
|
||||||
use futures::{stream::once, Future, Stream, StreamExt};
|
use futures::{stream::once, Future, Stream, StreamExt};
|
||||||
use hydration_context::SsrSharedContext;
|
use hydration_context::SsrSharedContext;
|
||||||
use leptos::{
|
use leptos::{
|
||||||
|
@ -61,12 +60,20 @@ use leptos_integration_utils::{
|
||||||
BoxedFnOnce, ExtendResponse, PinnedFuture, PinnedStream,
|
BoxedFnOnce, ExtendResponse, PinnedFuture, PinnedStream,
|
||||||
};
|
};
|
||||||
use leptos_meta::ServerMetaContext;
|
use leptos_meta::ServerMetaContext;
|
||||||
|
#[cfg(feature = "default")]
|
||||||
|
use leptos_router::static_routes::ResolvedStaticPath;
|
||||||
use leptos_router::{
|
use leptos_router::{
|
||||||
components::provide_server_redirect, location::RequestUrl, PathSegment,
|
components::provide_server_redirect,
|
||||||
RouteList, RouteListing, SsrMode, StaticDataMap, StaticMode,
|
location::RequestUrl,
|
||||||
|
static_routes::{RegenerationFn, StaticParamsMap},
|
||||||
|
PathSegment, RouteList, RouteListing, SsrMode,
|
||||||
};
|
};
|
||||||
|
#[cfg(feature = "default")]
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use server_fn::{redirect::REDIRECT_HEADER, ServerFnError};
|
use server_fn::{redirect::REDIRECT_HEADER, ServerFnError};
|
||||||
|
#[cfg(feature = "default")]
|
||||||
|
use std::path::Path;
|
||||||
use std::{fmt::Debug, io, pin::Pin, sync::Arc};
|
use std::{fmt::Debug, io, pin::Pin, sync::Arc};
|
||||||
#[cfg(feature = "default")]
|
#[cfg(feature = "default")]
|
||||||
use tower::ServiceExt;
|
use tower::ServiceExt;
|
||||||
|
@ -236,14 +243,20 @@ pub fn redirect(path: &str) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let msg = "Couldn't retrieve either Parts or ResponseOptions while \
|
|
||||||
trying to redirect().";
|
|
||||||
|
|
||||||
#[cfg(feature = "tracing")]
|
#[cfg(feature = "tracing")]
|
||||||
tracing::warn!("{}", &msg);
|
{
|
||||||
|
tracing::warn!(
|
||||||
|
"Couldn't retrieve either Parts or ResponseOptions while \
|
||||||
|
trying to redirect()."
|
||||||
|
);
|
||||||
|
}
|
||||||
#[cfg(not(feature = "tracing"))]
|
#[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",
|
feature = "tracing",
|
||||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||||
)]
|
)]
|
||||||
pub fn render_route<IV>(
|
pub fn render_route<S, IV>(
|
||||||
paths: Vec<AxumRouteListing>,
|
paths: Vec<AxumRouteListing>,
|
||||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||||
) -> impl Fn(
|
) -> impl Fn(
|
||||||
|
State<S>,
|
||||||
Request<Body>,
|
Request<Body>,
|
||||||
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
|
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
|
||||||
+ Clone
|
+ Clone
|
||||||
|
@ -508,6 +522,8 @@ pub fn render_route<IV>(
|
||||||
+ 'static
|
+ 'static
|
||||||
where
|
where
|
||||||
IV: IntoView + 'static,
|
IV: IntoView + 'static,
|
||||||
|
LeptosOptions: FromRef<S>,
|
||||||
|
S: Send + 'static,
|
||||||
{
|
{
|
||||||
render_route_with_context(paths, || {}, app_fn)
|
render_route_with_context(paths, || {}, app_fn)
|
||||||
}
|
}
|
||||||
|
@ -648,11 +664,12 @@ where
|
||||||
feature = "tracing",
|
feature = "tracing",
|
||||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
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>,
|
paths: Vec<AxumRouteListing>,
|
||||||
additional_context: impl Fn() + 'static + Clone + Send,
|
additional_context: impl Fn() + 'static + Clone + Send,
|
||||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||||
) -> impl Fn(
|
) -> impl Fn(
|
||||||
|
State<S>,
|
||||||
Request<Body>,
|
Request<Body>,
|
||||||
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
|
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
|
||||||
+ Clone
|
+ Clone
|
||||||
|
@ -660,6 +677,8 @@ pub fn render_route_with_context<IV>(
|
||||||
+ 'static
|
+ 'static
|
||||||
where
|
where
|
||||||
IV: IntoView + 'static,
|
IV: IntoView + 'static,
|
||||||
|
LeptosOptions: FromRef<S>,
|
||||||
|
S: Send + 'static,
|
||||||
{
|
{
|
||||||
let ooo = render_app_to_stream_with_context(
|
let ooo = render_app_to_stream_with_context(
|
||||||
additional_context.clone(),
|
additional_context.clone(),
|
||||||
|
@ -679,7 +698,7 @@ where
|
||||||
app_fn.clone(),
|
app_fn.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
move |req| {
|
move |state, req| {
|
||||||
// 1. Process route to match the values in routeListing
|
// 1. Process route to match the values in routeListing
|
||||||
let path = req
|
let path = req
|
||||||
.extensions()
|
.extensions()
|
||||||
|
@ -702,6 +721,25 @@ where
|
||||||
SsrMode::PartiallyBlocked => pb(req),
|
SsrMode::PartiallyBlocked => pb(req),
|
||||||
SsrMode::InOrder => io(req),
|
SsrMode::InOrder => io(req),
|
||||||
SsrMode::Async => asyn(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
|
where
|
||||||
IV: IntoView + 'static,
|
IV: IntoView + 'static,
|
||||||
{
|
{
|
||||||
handle_response(additional_context, app_fn, |app, chunks| {
|
handle_response(additional_context, app_fn, async_stream_builder)
|
||||||
Box::pin(async move {
|
}
|
||||||
let app = if cfg!(feature = "islands-router") {
|
|
||||||
app.to_html_stream_in_order_branching()
|
fn async_stream_builder<IV>(
|
||||||
} else {
|
app: IV,
|
||||||
app.to_html_stream_in_order()
|
chunks: BoxedFnOnce<PinnedStream<String>>,
|
||||||
};
|
) -> PinnedFuture<PinnedStream<String>>
|
||||||
let app = app.collect::<String>().await;
|
where
|
||||||
let chunks = chunks();
|
IV: IntoView + 'static,
|
||||||
Box::pin(once(async move { app }).chain(chunks))
|
{
|
||||||
as PinnedStream<String>
|
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)
|
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||||
)]
|
)]
|
||||||
pub fn generate_route_list<IV>(
|
pub fn generate_route_list<IV>(
|
||||||
app_fn: impl Fn() -> IV + 'static + Clone,
|
app_fn: impl Fn() -> IV + 'static + Clone + Send,
|
||||||
) -> Vec<AxumRouteListing>
|
) -> Vec<AxumRouteListing>
|
||||||
where
|
where
|
||||||
IV: IntoView + 'static,
|
IV: IntoView + 'static,
|
||||||
|
@ -1128,7 +1173,7 @@ where
|
||||||
generate_route_list_with_exclusions_and_ssg(app_fn, None).0
|
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
|
/// 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.
|
/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths.
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
|
@ -1136,8 +1181,8 @@ where
|
||||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||||
)]
|
)]
|
||||||
pub fn generate_route_list_with_ssg<IV>(
|
pub fn generate_route_list_with_ssg<IV>(
|
||||||
app_fn: impl Fn() -> IV + 'static + Clone,
|
app_fn: impl Fn() -> IV + 'static + Clone + Send,
|
||||||
) -> (Vec<AxumRouteListing>, StaticDataMap)
|
) -> (Vec<AxumRouteListing>, StaticRouteGenerator)
|
||||||
where
|
where
|
||||||
IV: IntoView + 'static,
|
IV: IntoView + 'static,
|
||||||
{
|
{
|
||||||
|
@ -1153,7 +1198,7 @@ where
|
||||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||||
)]
|
)]
|
||||||
pub fn generate_route_list_with_exclusions<IV>(
|
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>>,
|
excluded_routes: Option<Vec<String>>,
|
||||||
) -> Vec<AxumRouteListing>
|
) -> Vec<AxumRouteListing>
|
||||||
where
|
where
|
||||||
|
@ -1162,13 +1207,13 @@ where
|
||||||
generate_route_list_with_exclusions_and_ssg(app_fn, excluded_routes).0
|
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)]
|
#[allow(unused)]
|
||||||
pub async fn build_static_routes<IV>(
|
pub async fn build_static_routes<IV>(
|
||||||
options: &LeptosOptions,
|
options: &LeptosOptions,
|
||||||
app_fn: impl Fn() -> IV + 'static + Send + Clone,
|
app_fn: impl Fn() -> IV + 'static + Send + Clone,
|
||||||
routes: &[RouteListing],
|
routes: &[RouteListing],
|
||||||
static_data_map: StaticDataMap,
|
static_data_map: StaticParamsMap,
|
||||||
) where
|
) where
|
||||||
IV: IntoView + 'static,
|
IV: IntoView + 'static,
|
||||||
{
|
{
|
||||||
|
@ -1197,9 +1242,9 @@ pub async fn build_static_routes<IV>(
|
||||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||||
)]
|
)]
|
||||||
pub fn generate_route_list_with_exclusions_and_ssg<IV>(
|
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>>,
|
excluded_routes: Option<Vec<String>>,
|
||||||
) -> (Vec<AxumRouteListing>, StaticDataMap)
|
) -> (Vec<AxumRouteListing>, StaticRouteGenerator)
|
||||||
where
|
where
|
||||||
IV: IntoView + 'static,
|
IV: IntoView + 'static,
|
||||||
{
|
{
|
||||||
|
@ -1216,7 +1261,8 @@ pub struct AxumRouteListing {
|
||||||
path: String,
|
path: String,
|
||||||
mode: SsrMode,
|
mode: SsrMode,
|
||||||
methods: Vec<leptos_router::Method>,
|
methods: Vec<leptos_router::Method>,
|
||||||
static_mode: Option<(StaticMode, StaticDataMap)>,
|
#[allow(unused)]
|
||||||
|
regenerate: Vec<RegenerationFn>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<RouteListing> for AxumRouteListing {
|
impl From<RouteListing> for AxumRouteListing {
|
||||||
|
@ -1229,12 +1275,12 @@ impl From<RouteListing> for AxumRouteListing {
|
||||||
};
|
};
|
||||||
let mode = value.mode();
|
let mode = value.mode();
|
||||||
let methods = value.methods().collect();
|
let methods = value.methods().collect();
|
||||||
let static_mode = value.into_static_parts();
|
let regenerate = value.regenerate().into();
|
||||||
Self {
|
Self {
|
||||||
path,
|
path,
|
||||||
mode,
|
mode: mode.clone(),
|
||||||
methods,
|
methods,
|
||||||
static_mode,
|
regenerate,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1245,13 +1291,13 @@ impl AxumRouteListing {
|
||||||
path: String,
|
path: String,
|
||||||
mode: SsrMode,
|
mode: SsrMode,
|
||||||
methods: impl IntoIterator<Item = leptos_router::Method>,
|
methods: impl IntoIterator<Item = leptos_router::Method>,
|
||||||
static_mode: Option<(StaticMode, StaticDataMap)>,
|
regenerate: impl Into<Vec<RegenerationFn>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
path,
|
path,
|
||||||
mode,
|
mode,
|
||||||
methods: methods.into_iter().collect(),
|
methods: methods.into_iter().collect(),
|
||||||
static_mode,
|
regenerate: regenerate.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1261,20 +1307,14 @@ impl AxumRouteListing {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The rendering mode for this path.
|
/// The rendering mode for this path.
|
||||||
pub fn mode(&self) -> SsrMode {
|
pub fn mode(&self) -> &SsrMode {
|
||||||
self.mode
|
&self.mode
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The HTTP request methods this path can handle.
|
/// The HTTP request methods this path can handle.
|
||||||
pub fn methods(&self) -> impl Iterator<Item = leptos_router::Method> + '_ {
|
pub fn methods(&self) -> impl Iterator<Item = leptos_router::Method> + '_ {
|
||||||
self.methods.iter().copied()
|
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
|
/// 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)
|
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||||
)]
|
)]
|
||||||
pub fn generate_route_list_with_exclusions_and_ssg_and_context<IV>(
|
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>>,
|
excluded_routes: Option<Vec<String>>,
|
||||||
additional_context: impl Fn() + 'static + Clone,
|
additional_context: impl Fn() + Clone + Send + 'static,
|
||||||
) -> (Vec<AxumRouteListing>, StaticDataMap)
|
) -> (Vec<AxumRouteListing>, StaticRouteGenerator)
|
||||||
where
|
where
|
||||||
IV: IntoView + 'static,
|
IV: IntoView + 'static,
|
||||||
{
|
{
|
||||||
|
// do some basic reactive setup
|
||||||
init_executor();
|
init_executor();
|
||||||
|
|
||||||
let owner = Owner::new_root(Some(Arc::new(SsrSharedContext::new())));
|
let owner = Owner::new_root(Some(Arc::new(SsrSharedContext::new())));
|
||||||
|
|
||||||
let routes = owner
|
let routes = owner
|
||||||
.with(|| {
|
.with(|| {
|
||||||
// stub out a path for now
|
// stub out a path for now
|
||||||
|
@ -1310,6 +1351,12 @@ where
|
||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let generator = StaticRouteGenerator::new(
|
||||||
|
&routes,
|
||||||
|
app_fn.clone(),
|
||||||
|
additional_context.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
// Axum's Router defines Root routes as "/" not ""
|
// Axum's Router defines Root routes as "/" not ""
|
||||||
let mut routes = routes
|
let mut routes = routes
|
||||||
.into_inner()
|
.into_inner()
|
||||||
|
@ -1323,7 +1370,7 @@ where
|
||||||
"/".to_string(),
|
"/".to_string(),
|
||||||
Default::default(),
|
Default::default(),
|
||||||
[leptos_router::Method::Get],
|
[leptos_router::Method::Get],
|
||||||
None,
|
vec![],
|
||||||
)]
|
)]
|
||||||
} else {
|
} else {
|
||||||
// Routes to exclude from auto generation
|
// Routes to exclude from auto generation
|
||||||
|
@ -1333,16 +1380,284 @@ where
|
||||||
}
|
}
|
||||||
routes
|
routes
|
||||||
},
|
},
|
||||||
StaticDataMap::new(), // TODO
|
generator,
|
||||||
//static_data_map,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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
|
/// 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.
|
/// having to use wildcards or manually define all routes in multiple places.
|
||||||
pub trait LeptosRoutes<S>
|
pub trait LeptosRoutes<S>
|
||||||
where
|
where
|
||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
|
LeptosOptions: FromRef<S>,
|
||||||
{
|
{
|
||||||
fn leptos_routes<IV>(
|
fn leptos_routes<IV>(
|
||||||
self,
|
self,
|
||||||
|
@ -1372,209 +1687,6 @@ where
|
||||||
H: axum::handler::Handler<T, S>,
|
H: axum::handler::Handler<T, S>,
|
||||||
T: 'static;
|
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 {
|
trait AxumPath {
|
||||||
fn to_axum_path(&self) -> String;
|
fn to_axum_path(&self) -> String;
|
||||||
|
@ -1611,6 +1723,7 @@ impl AxumPath for &[PathSegment] {
|
||||||
impl<S> LeptosRoutes<S> for axum::Router<S>
|
impl<S> LeptosRoutes<S> for axum::Router<S>
|
||||||
where
|
where
|
||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
|
LeptosOptions: FromRef<S>,
|
||||||
{
|
{
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
feature = "tracing",
|
feature = "tracing",
|
||||||
|
@ -1688,25 +1801,24 @@ where
|
||||||
provide_context(method);
|
provide_context(method);
|
||||||
cx_with_state();
|
cx_with_state();
|
||||||
};
|
};
|
||||||
router = if let Some(static_mode) = listing.static_mode() {
|
router = if matches!(listing.mode(), SsrMode::Static(_)) {
|
||||||
#[cfg(feature = "default")]
|
#[cfg(feature = "default")]
|
||||||
{
|
{
|
||||||
static_route(
|
router.route(
|
||||||
router,
|
|
||||||
path,
|
path,
|
||||||
app_fn.clone(),
|
get(handle_static_route(
|
||||||
cx_with_state_and_method.clone(),
|
cx_with_state_and_method.clone(),
|
||||||
method,
|
app_fn.clone(),
|
||||||
static_mode,
|
listing.regenerate.clone(),
|
||||||
|
)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
#[cfg(not(feature = "default"))]
|
#[cfg(not(feature = "default"))]
|
||||||
{
|
{
|
||||||
_ = static_mode;
|
|
||||||
panic!(
|
panic!(
|
||||||
"Static site generation is not currently \
|
"Static routes are not currently supported on \
|
||||||
supported on WASM32 server targets."
|
WASM32 server targets."
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
router.route(
|
router.route(
|
||||||
|
@ -1765,6 +1877,7 @@ where
|
||||||
leptos_router::Method::Patch => patch(s),
|
leptos_router::Method::Patch => patch(s),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_ => unreachable!()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,6 +5,7 @@ use leptos::{
|
||||||
reactive_graph::owner::{Owner, Sandboxed},
|
reactive_graph::owner::{Owner, Sandboxed},
|
||||||
IntoView,
|
IntoView,
|
||||||
};
|
};
|
||||||
|
use leptos_config::LeptosOptions;
|
||||||
use leptos_meta::ServerMetaContextOutput;
|
use leptos_meta::ServerMetaContextOutput;
|
||||||
use std::{future::Future, pin::Pin, sync::Arc};
|
use std::{future::Future, pin::Pin, sync::Arc};
|
||||||
|
|
||||||
|
@ -132,3 +133,13 @@ where
|
||||||
}));
|
}));
|
||||||
(owner, stream)
|
(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();
|
buf.next_id();
|
||||||
let suspense_context = use_context::<SuspenseContext>().unwrap();
|
let suspense_context = use_context::<SuspenseContext>().unwrap();
|
||||||
|
|
||||||
let owner = Owner::current().unwrap();
|
let owner = Owner::current().unwrap();
|
||||||
|
|
||||||
// we need to wait for one of two things: either
|
// we need to wait for one of two things: either
|
||||||
|
@ -277,6 +276,16 @@ where
|
||||||
futures::channel::oneshot::channel::<()>();
|
futures::channel::oneshot::channel::<()>();
|
||||||
|
|
||||||
let mut tasks_tx = Some(tasks_tx);
|
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({
|
let eff = reactive_graph::effect::RenderEffect::new_isomorphic({
|
||||||
move |_| {
|
move |_| {
|
||||||
tasks.track();
|
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(
|
let mut fut = Box::pin(ScopedFuture::new(ErrorHookFuture::new(
|
||||||
async move {
|
async move {
|
||||||
// race the local resource notifier against the set of tasks
|
// race the local resource notifier against the set of tasks
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
use crate::owner::Owner;
|
use crate::owner::Owner;
|
||||||
use or_poisoned::OrPoisoned;
|
use or_poisoned::OrPoisoned;
|
||||||
use std::any::{Any, TypeId};
|
use std::{
|
||||||
|
any::{Any, TypeId},
|
||||||
|
collections::VecDeque,
|
||||||
|
};
|
||||||
|
|
||||||
impl Owner {
|
impl Owner {
|
||||||
fn provide_context<T: Send + Sync + 'static>(&self, value: T) {
|
fn provide_context<T: Send + Sync + 'static>(&self, value: T) {
|
||||||
|
@ -60,6 +63,35 @@ impl Owner {
|
||||||
None
|
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`]
|
/// Provides a context value of type `T` to the current reactive [`Owner`]
|
||||||
|
|
|
@ -28,6 +28,7 @@ send_wrapper = "0.6.0"
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
percent-encoding = { version = "2.3", optional = true }
|
percent-encoding = { version = "2.3", optional = true }
|
||||||
gloo-net = "0.6.0"
|
gloo-net = "0.6.0"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
|
||||||
[dependencies.web-sys]
|
[dependencies.web-sys]
|
||||||
version = "0.3.70"
|
version = "0.3.70"
|
||||||
|
|
|
@ -450,6 +450,12 @@ pub fn Redirect<P>(
|
||||||
"Calling <Redirect/> without a ServerRedirectFunction \
|
"Calling <Redirect/> without a ServerRedirectFunction \
|
||||||
provided, in SSR mode."
|
provided, in SSR mode."
|
||||||
);
|
);
|
||||||
|
|
||||||
|
#[cfg(not(feature = "tracing"))]
|
||||||
|
eprintln!(
|
||||||
|
"Calling <Redirect/> without a ServerRedirectFunction \
|
||||||
|
provided, in SSR mode."
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let navigate = use_navigate();
|
let navigate = use_navigate();
|
||||||
|
|
|
@ -2,8 +2,8 @@ use crate::{
|
||||||
location::{LocationProvider, Url},
|
location::{LocationProvider, Url},
|
||||||
matching::Routes,
|
matching::Routes,
|
||||||
params::ParamsMap,
|
params::ParamsMap,
|
||||||
ChooseView, MatchInterface, MatchNestedRoutes, MatchParams, Method,
|
ChooseView, MatchInterface, MatchNestedRoutes, MatchParams, PathSegment,
|
||||||
PathSegment, RouteList, RouteListing, RouteMatchId,
|
RouteList, RouteListing, RouteMatchId,
|
||||||
};
|
};
|
||||||
use any_spawner::Executor;
|
use any_spawner::Executor;
|
||||||
use either_of::{Either, EitherOf3};
|
use either_of::{Either, EitherOf3};
|
||||||
|
@ -511,10 +511,8 @@ where
|
||||||
RouteListing::new(
|
RouteListing::new(
|
||||||
path,
|
path,
|
||||||
data.ssr_mode,
|
data.ssr_mode,
|
||||||
// TODO methods
|
data.methods,
|
||||||
[Method::Get],
|
data.regenerate,
|
||||||
// TODO static data
|
|
||||||
None,
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
|
@ -1,9 +1,17 @@
|
||||||
use crate::{
|
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::{
|
use std::{
|
||||||
cell::{Cell, RefCell},
|
cell::{Cell, RefCell},
|
||||||
collections::HashSet,
|
collections::HashSet,
|
||||||
|
future::Future,
|
||||||
|
mem,
|
||||||
};
|
};
|
||||||
use tachys::{renderer::Renderer, view::RenderHtml};
|
use tachys::{renderer::Renderer, view::RenderHtml};
|
||||||
|
|
||||||
|
@ -13,7 +21,7 @@ pub struct RouteListing {
|
||||||
path: Vec<PathSegment>,
|
path: Vec<PathSegment>,
|
||||||
mode: SsrMode,
|
mode: SsrMode,
|
||||||
methods: HashSet<Method>,
|
methods: HashSet<Method>,
|
||||||
static_mode: Option<(StaticMode, StaticDataMap)>,
|
regenerate: Vec<RegenerationFn>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RouteListing {
|
impl RouteListing {
|
||||||
|
@ -22,19 +30,19 @@ impl RouteListing {
|
||||||
path: impl IntoIterator<Item = PathSegment>,
|
path: impl IntoIterator<Item = PathSegment>,
|
||||||
mode: SsrMode,
|
mode: SsrMode,
|
||||||
methods: impl IntoIterator<Item = Method>,
|
methods: impl IntoIterator<Item = Method>,
|
||||||
static_mode: Option<(StaticMode, StaticDataMap)>,
|
regenerate: impl IntoIterator<Item = RegenerationFn>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
path: path.into_iter().collect(),
|
path: path.into_iter().collect(),
|
||||||
mode,
|
mode,
|
||||||
methods: methods.into_iter().collect(),
|
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.
|
/// 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 {
|
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.
|
/// The path this route handles.
|
||||||
|
@ -43,8 +51,8 @@ impl RouteListing {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The rendering mode for this path.
|
/// The rendering mode for this path.
|
||||||
pub fn mode(&self) -> SsrMode {
|
pub fn mode(&self) -> &SsrMode {
|
||||||
self.mode
|
&self.mode
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The HTTP request methods this path can handle.
|
/// The HTTP request methods this path can handle.
|
||||||
|
@ -52,56 +60,95 @@ impl RouteListing {
|
||||||
self.methods.iter().copied()
|
self.methods.iter().copied()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether this route is statically rendered.
|
/// The set of regeneration functions that should be applied to this route, if it is statically
|
||||||
#[inline(always)]
|
/// generated (either up front or incrementally).
|
||||||
pub fn static_mode(&self) -> Option<StaticMode> {
|
pub fn regenerate(&self) -> &[RegenerationFn] {
|
||||||
self.static_mode.as_ref().map(|n| n.0)
|
&self.regenerate
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether this route is statically rendered.
|
/// Whether this route is statically rendered.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn static_data_map(&self) -> Option<&StaticDataMap> {
|
pub fn static_route(&self) -> Option<&StaticRoute> {
|
||||||
self.static_mode.as_ref().map(|n| &n.1)
|
match self.mode {
|
||||||
|
SsrMode::Static(ref route) => Some(route),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn into_static_parts(self) -> Option<(StaticMode, StaticDataMap)> {
|
pub async fn into_static_paths(self) -> Option<Vec<ResolvedStaticPath>> {
|
||||||
self.static_mode
|
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
|
/// 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
|
/// is not marked as statically rendered. All route parameters to use when resolving all paths
|
||||||
/// to render should be passed in the `params` argument.
|
/// to render should be passed in the `params` argument.
|
||||||
pub async fn build_static<IV>(
|
pub async fn build_static<IV>(
|
||||||
&self,
|
&self,
|
||||||
options: &LeptosOptions,
|
options: &LeptosOptions,
|
||||||
app_fn: impl Fn() -> IV + Send + 'static + Clone,
|
app_fn: impl Fn() -> IV + Send + 'static + Clone,
|
||||||
additional_context: impl Fn() + Send + 'static + Clone,
|
additional_context: impl Fn() + Send + 'static + Clone,
|
||||||
params: &StaticParamsMap,
|
params: &StaticParamsMap,
|
||||||
) -> Result<bool, std::io::Error>
|
) -> Result<bool, std::io::Error>
|
||||||
where
|
where
|
||||||
IV: IntoView + 'static,
|
IV: IntoView + 'static,
|
||||||
{
|
{
|
||||||
match self.static_mode {
|
match self.mode {
|
||||||
None => Ok(false),
|
SsrMode::Static(route) => {
|
||||||
Some(_) => {
|
let mut path = StaticPath::new(self.path.clone());
|
||||||
let mut path = StaticPath::new(&self.leptos_path);
|
for path in path.into_paths(params) {
|
||||||
path.add_params(params);
|
/*path.write(
|
||||||
for path in path.into_paths() {
|
options,
|
||||||
path.write(
|
app_fn.clone(),
|
||||||
options,
|
additional_context.clone(),
|
||||||
app_fn.clone(),
|
)
|
||||||
additional_context.clone(),
|
.await?;*/ println!()
|
||||||
)
|
}
|
||||||
.await?;
|
Ok(true)
|
||||||
}
|
}
|
||||||
Ok(true)
|
_ => Ok(false),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default, Clone)]
|
||||||
pub struct RouteList(Vec<RouteListing>);
|
pub struct RouteList(Vec<RouteListing>);
|
||||||
|
|
||||||
impl From<Vec<RouteListing>> for RouteList {
|
impl From<Vec<RouteListing>> for RouteList {
|
||||||
|
@ -124,6 +171,45 @@ impl RouteList {
|
||||||
pub fn into_inner(self) -> Vec<RouteListing> {
|
pub fn into_inner(self) -> Vec<RouteListing> {
|
||||||
self.0
|
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 {
|
impl RouteList {
|
||||||
|
|
|
@ -155,8 +155,10 @@ pub fn use_location() -> Location {
|
||||||
location
|
location
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) type RawParamsMap = ArcMemo<ParamsMap>;
|
||||||
|
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
fn use_params_raw() -> ArcMemo<ParamsMap> {
|
fn use_params_raw() -> RawParamsMap {
|
||||||
use_context().expect(
|
use_context().expect(
|
||||||
"Tried to access params outside the context of a matched <Route>.",
|
"Tried to access params outside the context of a matched <Route>.",
|
||||||
)
|
)
|
||||||
|
|
|
@ -16,7 +16,7 @@ pub mod nested_router;
|
||||||
pub mod params;
|
pub mod params;
|
||||||
//mod router;
|
//mod router;
|
||||||
mod ssr_mode;
|
mod ssr_mode;
|
||||||
mod static_route;
|
pub mod static_routes;
|
||||||
|
|
||||||
pub use generate_route_list::*;
|
pub use generate_route_list::*;
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
|
@ -26,4 +26,3 @@ pub use method::*;
|
||||||
pub use navigate::*;
|
pub use navigate::*;
|
||||||
//pub use router::*;
|
//pub use router::*;
|
||||||
pub use ssr_mode::*;
|
pub use ssr_mode::*;
|
||||||
pub use static_route::*;
|
|
||||||
|
|
|
@ -201,10 +201,29 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
pub(crate) fn unescape(s: &str) -> String {
|
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()
|
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>(
|
pub(crate) fn handle_anchor_click<NavFn, NavFut>(
|
||||||
router_base: Option<Cow<'static, str>>,
|
router_base: Option<Cow<'static, str>>,
|
||||||
parse_with_base: fn(&str, &str) -> Result<Url, JsValue>,
|
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 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
|
// let browser handle this event if it leaves our domain
|
||||||
// or our base path
|
// or our base path
|
||||||
|
|
|
@ -6,10 +6,10 @@ pub use path_segment::*;
|
||||||
mod horizontal;
|
mod horizontal;
|
||||||
mod nested;
|
mod nested;
|
||||||
mod vertical;
|
mod vertical;
|
||||||
use crate::SsrMode;
|
use crate::{static_routes::RegenerationFn, Method, SsrMode};
|
||||||
pub use horizontal::*;
|
pub use horizontal::*;
|
||||||
pub use nested::*;
|
pub use nested::*;
|
||||||
use std::{borrow::Cow, marker::PhantomData};
|
use std::{borrow::Cow, collections::HashSet, marker::PhantomData};
|
||||||
use tachys::{
|
use tachys::{
|
||||||
renderer::Renderer,
|
renderer::Renderer,
|
||||||
view::{Render, RenderHtml},
|
view::{Render, RenderHtml},
|
||||||
|
@ -145,6 +145,8 @@ where
|
||||||
pub struct GeneratedRouteData {
|
pub struct GeneratedRouteData {
|
||||||
pub segments: Vec<PathSegment>,
|
pub segments: Vec<PathSegment>,
|
||||||
pub ssr_mode: SsrMode,
|
pub ssr_mode: SsrMode,
|
||||||
|
pub methods: HashSet<Method>,
|
||||||
|
pub regenerate: Vec<RegenerationFn>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -2,11 +2,12 @@ use super::{
|
||||||
MatchInterface, MatchNestedRoutes, PartialPathMatch, PathSegment,
|
MatchInterface, MatchNestedRoutes, PartialPathMatch, PathSegment,
|
||||||
PossibleRouteMatch, RouteMatchId,
|
PossibleRouteMatch, RouteMatchId,
|
||||||
};
|
};
|
||||||
use crate::{ChooseView, GeneratedRouteData, MatchParams, SsrMode};
|
use crate::{ChooseView, GeneratedRouteData, MatchParams, Method, SsrMode};
|
||||||
use core::{fmt, iter};
|
use core::{fmt, iter};
|
||||||
use either_of::Either;
|
use either_of::Either;
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
|
collections::HashSet,
|
||||||
marker::PhantomData,
|
marker::PhantomData,
|
||||||
sync::atomic::{AtomicU16, Ordering},
|
sync::atomic::{AtomicU16, Ordering},
|
||||||
};
|
};
|
||||||
|
@ -19,7 +20,7 @@ mod tuples;
|
||||||
|
|
||||||
static ROUTE_ID: AtomicU16 = AtomicU16::new(1);
|
static ROUTE_ID: AtomicU16 = AtomicU16::new(1);
|
||||||
|
|
||||||
#[derive(Debug, Copy, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub struct NestedRoute<Segments, Children, Data, View, R> {
|
pub struct NestedRoute<Segments, Children, Data, View, R> {
|
||||||
id: u16,
|
id: u16,
|
||||||
segments: Segments,
|
segments: Segments,
|
||||||
|
@ -27,6 +28,7 @@ pub struct NestedRoute<Segments, Children, Data, View, R> {
|
||||||
data: Data,
|
data: Data,
|
||||||
view: View,
|
view: View,
|
||||||
rndr: PhantomData<R>,
|
rndr: PhantomData<R>,
|
||||||
|
methods: HashSet<Method>,
|
||||||
ssr_mode: SsrMode,
|
ssr_mode: SsrMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,7 +48,8 @@ where
|
||||||
data: self.data.clone(),
|
data: self.data.clone(),
|
||||||
view: self.view.clone(),
|
view: self.view.clone(),
|
||||||
rndr: PhantomData,
|
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: (),
|
data: (),
|
||||||
view,
|
view,
|
||||||
rndr: PhantomData,
|
rndr: PhantomData,
|
||||||
|
methods: [Method::Get].into(),
|
||||||
ssr_mode: Default::default(),
|
ssr_mode: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -81,6 +85,7 @@ impl<Segments, Data, View, R> NestedRoute<Segments, (), Data, View, R> {
|
||||||
view,
|
view,
|
||||||
rndr,
|
rndr,
|
||||||
ssr_mode,
|
ssr_mode,
|
||||||
|
methods,
|
||||||
..
|
..
|
||||||
} = self;
|
} = self;
|
||||||
NestedRoute {
|
NestedRoute {
|
||||||
|
@ -90,6 +95,7 @@ impl<Segments, Data, View, R> NestedRoute<Segments, (), Data, View, R> {
|
||||||
data,
|
data,
|
||||||
view,
|
view,
|
||||||
ssr_mode,
|
ssr_mode,
|
||||||
|
methods,
|
||||||
rndr,
|
rndr,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -249,25 +255,44 @@ where
|
||||||
let mut segment_routes = Vec::new();
|
let mut segment_routes = Vec::new();
|
||||||
self.segments.generate_path(&mut segment_routes);
|
self.segments.generate_path(&mut segment_routes);
|
||||||
let children = self.children.as_ref();
|
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 {
|
match children {
|
||||||
None => Either::Left(iter::once(GeneratedRouteData {
|
None => Either::Left(iter::once(GeneratedRouteData {
|
||||||
segments: segment_routes,
|
segments: segment_routes,
|
||||||
ssr_mode
|
ssr_mode,
|
||||||
|
methods,
|
||||||
|
regenerate
|
||||||
})),
|
})),
|
||||||
Some(children) => {
|
Some(children) => {
|
||||||
Either::Right(children.generate_routes().into_iter().map(move |child| {
|
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 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 {
|
if child.ssr_mode > ssr_mode {
|
||||||
GeneratedRouteData {
|
GeneratedRouteData {
|
||||||
segments,
|
segments,
|
||||||
ssr_mode: child.ssr_mode,
|
ssr_mode: child.ssr_mode,
|
||||||
|
methods, regenerate
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
GeneratedRouteData {
|
GeneratedRouteData {
|
||||||
segments,
|
segments,
|
||||||
ssr_mode,
|
ssr_mode: ssr_mode.clone(), methods, regenerate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
|
@ -3,8 +3,8 @@ use crate::{
|
||||||
location::{LocationProvider, Url},
|
location::{LocationProvider, Url},
|
||||||
matching::Routes,
|
matching::Routes,
|
||||||
params::ParamsMap,
|
params::ParamsMap,
|
||||||
ChooseView, MatchInterface, MatchNestedRoutes, MatchParams, Method,
|
ChooseView, MatchInterface, MatchNestedRoutes, MatchParams, PathSegment,
|
||||||
PathSegment, RouteList, RouteListing, RouteMatchId,
|
RouteList, RouteListing, RouteMatchId,
|
||||||
};
|
};
|
||||||
use any_spawner::Executor;
|
use any_spawner::Executor;
|
||||||
use either_of::{Either, EitherOf3};
|
use either_of::{Either, EitherOf3};
|
||||||
|
@ -272,10 +272,8 @@ where
|
||||||
RouteListing::new(
|
RouteListing::new(
|
||||||
path,
|
path,
|
||||||
data.ssr_mode,
|
data.ssr_mode,
|
||||||
// TODO methods
|
data.methods,
|
||||||
[Method::Get],
|
data.regenerate,
|
||||||
// TODO static data
|
|
||||||
None,
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.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.
|
/// 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
|
/// 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.
|
/// 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.
|
/// - *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.
|
/// - *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
|
/// 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
|
/// 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`.)
|
/// 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 {
|
pub enum SsrMode {
|
||||||
#[default]
|
#[default]
|
||||||
OutOfOrder,
|
OutOfOrder,
|
||||||
PartiallyBlocked,
|
PartiallyBlocked,
|
||||||
InOrder,
|
InOrder,
|
||||||
Async,
|
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(['"', '/'])
|
lit.trim_start_matches(['"', '/'])
|
||||||
.trim_end_matches(['"', '/']),
|
.trim_end_matches(['"', '/']),
|
||||||
);
|
);
|
||||||
|
if lit.ends_with(r#"/""#) && lit != r#""/""# {
|
||||||
|
self.segments.push(Segment::Static("/".to_string()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
TokenTree::Group(_) => unimplemented!(),
|
TokenTree::Group(_) => unimplemented!(),
|
||||||
TokenTree::Ident(_) => unimplemented!(),
|
TokenTree::Ident(_) => unimplemented!(),
|
||||||
|
@ -102,13 +105,14 @@ impl SegmentParser {
|
||||||
|
|
||||||
impl Segment {
|
impl Segment {
|
||||||
fn is_valid(segment: &str) -> bool {
|
fn is_valid(segment: &str) -> bool {
|
||||||
segment.chars().all(|c| {
|
segment == "/"
|
||||||
c.is_ascii_digit()
|
|| segment.chars().all(|c| {
|
||||||
|| c.is_ascii_lowercase()
|
c.is_ascii_digit()
|
||||||
|| c.is_ascii_uppercase()
|
|| c.is_ascii_lowercase()
|
||||||
|| RFC3986_UNRESERVED.contains(&c)
|
|| c.is_ascii_uppercase()
|
||||||
|| RFC3986_PCHAR_OTHER.contains(&c)
|
|| RFC3986_UNRESERVED.contains(&c)
|
||||||
})
|
|| RFC3986_PCHAR_OTHER.contains(&c)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ensure_valid(&self) {
|
fn ensure_valid(&self) {
|
||||||
|
|
|
@ -64,14 +64,14 @@ fn parses_no_slashes() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parses_no_leading_slash() {
|
fn parses_no_leading_slash() {
|
||||||
let output = path!("home/");
|
let output = path!("home");
|
||||||
assert_eq!(output, (StaticSegment("home"),));
|
assert_eq!(output, (StaticSegment("home"),));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parses_trailing_slash() {
|
fn parses_trailing_slash() {
|
||||||
let output = path!("/home/");
|
let output = path!("/home/");
|
||||||
assert_eq!(output, (StaticSegment("home"),));
|
assert_eq!(output, (StaticSegment("home"), StaticSegment("/")));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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]
|
#[test]
|
||||||
fn parses_consecutive_static() {
|
fn parses_consecutive_static() {
|
||||||
let output = path!("/foo/bar/baz");
|
let output = path!("/foo/bar/baz");
|
||||||
|
|
Loading…
Reference in a new issue