Compare commits

..

15 Commits

Author SHA1 Message Date
Greg Johnston
67ccd37336 fix: remove browser-only SendWrapper items during either kind of Suspense (closes #2907) 2024-09-02 07:57:03 -04:00
Greg Johnston
3b1b2e2dcc fix version for publish 2024-08-31 11:59:05 -04:00
Greg Johnston
7831e4ad05 next beta release 2024-08-31 11:59:05 -04:00
Greg Johnston
e7bb859cd9 feat: add support for static routing and incremental static regeneration (#2875) 2024-08-31 10:33:12 -04:00
Rakshith Ravi
9fc26e609c feat: allow for documentation and other attributes to fields in server fn (#2876) 2024-08-31 09:35:08 -04:00
mrvillage
4f1ee65e6c Add hash files support to 0.7 (#2894)
* Add support for JS and WASM file name hashing

* Add `<HashedStylesheet />`

* Update `<HashedStylesheet />`

* Whoops

* Fix formatting

* My IDE is just refusing to work apparently

* I hate my IDE

* Don't run the doctest

* Just remove the example, I don't know enough about doctest for this
2024-08-30 14:29:59 -07:00
Greg Johnston
ceff827a77 Merge pull request #2884 from leptos-rs/rstml-0.12
update to rstml and improve recoverability in attributes
2024-08-28 07:46:08 -04:00
Álvaro Mondéjar Rubio
a7db918775 docs: update main documentation of leptos crate (#2853) 2024-08-28 07:44:29 -04:00
Baptiste
be20ecd366 fix: implement Copy and Clone for HtmlElement without needing Rndr to be Clone/copy (#2889) 2024-08-28 07:25:51 -04:00
Greg Johnston
5790d8ad12 fix: derive various traits on Dom to make it easier to derive traits on structs that take a generic Renderer 2024-08-28 07:25:08 -04:00
luoxiaozero
7dc58e248c fix: compile attr:aria-* syntax (#2887) 2024-08-27 09:00:13 -07:00
Greg Johnston
7b03e63b23 docs: add note about curly braces 2024-08-26 21:00:45 -04:00
Greg Johnston
55fd7c6421 chore: clippy 2024-08-26 20:57:02 -04:00
Greg Johnston
ba1ea4c2bb do not error on unbraced 2024-08-26 20:56:23 -04:00
blorbb
6a4fc96835 rstml 0.12 and enforce braces in attribute values 2024-08-26 20:55:12 -04:00
69 changed files with 2344 additions and 846 deletions

View File

@@ -40,36 +40,36 @@ members = [
exclude = ["benchmarks", "examples", "projects"]
[workspace.package]
version = "0.7.0-beta2"
version = "0.7.0-beta4"
edition = "2021"
rust-version = "1.76"
[workspace.dependencies]
throw_error = { path = "./any_error/", version = "0.2.0-beta2" }
throw_error = { path = "./any_error/", version = "0.2.0-beta4" }
any_spawner = { path = "./any_spawner/", version = "0.1.0" }
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1.0" }
either_of = { path = "./either_of/", version = "0.1.0" }
hydration_context = { path = "./hydration_context", version = "0.2.0-beta2" }
leptos = { path = "./leptos", version = "0.7.0-beta2" }
leptos_config = { path = "./leptos_config", version = "0.7.0-beta2" }
leptos_dom = { path = "./leptos_dom", version = "0.7.0-beta2" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.0-beta2" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.0-beta2" }
leptos_macro = { path = "./leptos_macro", version = "0.7.0-beta2" }
leptos_router = { path = "./router", version = "0.7.0-beta2" }
leptos_router_macro = { path = "./router_macro", version = "0.7.0-beta2" }
leptos_server = { path = "./leptos_server", version = "0.7.0-beta2" }
leptos_meta = { path = "./meta", version = "0.7.0-beta2" }
next_tuple = { path = "./next_tuple", version = "0.1.0-beta2" }
hydration_context = { path = "./hydration_context", version = "0.2.0-beta4" }
leptos = { path = "./leptos", version = "0.7.0-beta4" }
leptos_config = { path = "./leptos_config", version = "0.7.0-beta4" }
leptos_dom = { path = "./leptos_dom", version = "0.7.0-beta4" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.0-beta4" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.0-beta4" }
leptos_macro = { path = "./leptos_macro", version = "0.7.0-beta4" }
leptos_router = { path = "./router", version = "0.7.0-beta4" }
leptos_router_macro = { path = "./router_macro", version = "0.7.0-beta4" }
leptos_server = { path = "./leptos_server", version = "0.7.0-beta4" }
leptos_meta = { path = "./meta", version = "0.7.0-beta4" }
next_tuple = { path = "./next_tuple", version = "0.1.0-beta4" }
oco_ref = { path = "./oco", version = "0.2.0" }
or_poisoned = { path = "./or_poisoned", version = "0.1.0" }
reactive_graph = { path = "./reactive_graph", version = "0.1.0-beta2" }
reactive_stores = { path = "./reactive_stores", version = "0.1.0-beta2" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0-beta2" }
server_fn = { path = "./server_fn", version = "0.7.0-beta2" }
server_fn_macro = { path = "./server_fn_macro", version = "0.7.0-beta2" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.0-beta2" }
tachys = { path = "./tachys", version = "0.1.0-beta2" }
reactive_graph = { path = "./reactive_graph", version = "0.1.0-beta4" }
reactive_stores = { path = "./reactive_stores", version = "0.1.0-beta4" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0-beta4" }
server_fn = { path = "./server_fn", version = "0.7.0-beta4" }
server_fn_macro = { path = "./server_fn_macro", version = "0.7.0-beta4" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.0-beta4" }
tachys = { path = "./tachys", version = "0.1.0-beta4" }
[profile.release]
codegen-units = 1

View File

@@ -1,6 +1,6 @@
[package]
name = "throw_error"
version = "0.2.0-beta2"
version = "0.2.0-beta4"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -1,4 +1,4 @@
use leptos::prelude::{signal::*, *};
use leptos::prelude::*;
const MANY_COUNTERS: usize = 1000;

13
examples/static_routing/.gitignore vendored Normal file
View 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/

View 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"

View 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.

View File

@@ -0,0 +1,8 @@
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/cargo-leptos.toml" },
]
[env]
CLIENT_PROCESS_NAME = "ssr_modes_axum"

View 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,3 @@
# My first blog post
Having a blog is *fun*.

View File

@@ -0,0 +1,3 @@
# My second blog post
Coming up with content is hard.

View File

@@ -0,0 +1,3 @@
# My third blog post
Could I just have AI write this for me instead?

View File

@@ -0,0 +1,3 @@
# My fourth post
Here is some content. It should regenerate the static page.

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "stable" # test change

View 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
}

View 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);
}

View 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
}

View File

@@ -0,0 +1,3 @@
body {
font-family: sans-serif;
}

View File

@@ -1,6 +1,6 @@
[package]
name = "hydration_context"
version = "0.2.0-beta2"
version = "0.2.0-beta4"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -10,6 +10,7 @@ edition.workspace = true
[dependencies]
actix-http = "3.8"
actix-files = "0.6"
actix-web = "4.8"
futures = "0.3.30"
any_spawner = { workspace = true, features = ["tokio"] }
@@ -25,6 +26,8 @@ parking_lot = "0.12.3"
tracing = { version = "0.1", optional = true }
tokio = { version = "1.39", features = ["rt", "fs"] }
send_wrapper = "0.6.0"
dashmap = "6"
once_cell = "1"
[package.metadata.docs.rs]
rustdoc-args = ["--generate-link-to-definition"]

View File

@@ -6,30 +6,38 @@
//! [`examples`](https://github.com/leptos-rs/leptos/tree/main/examples)
//! directory in the Leptos repository.
use actix_files::NamedFile;
use actix_http::header::{HeaderName, HeaderValue, ACCEPT, LOCATION, REFERER};
use actix_web::{
body::BoxBody,
dev::{ServiceFactory, ServiceRequest},
http::header,
web::{Payload, ServiceConfig},
test,
web::{Data, Payload, ServiceConfig},
*,
};
use dashmap::DashMap;
use futures::{stream::once, Stream, StreamExt};
use http::StatusCode;
use hydration_context::SsrSharedContext;
use leptos::{
config::LeptosOptions,
context::{provide_context, use_context},
prelude::expect_context,
reactive_graph::{computed::ScopedFuture, owner::Owner},
IntoView, *,
IntoView,
};
use leptos_integration_utils::{
BoxedFnOnce, ExtendResponse, PinnedFuture, PinnedStream,
};
use leptos_meta::ServerMetaContext;
use leptos_router::{
components::provide_server_redirect, location::RequestUrl, PathSegment,
RouteList, RouteListing, SsrMode, StaticDataMap, StaticMode, *,
components::provide_server_redirect,
location::RequestUrl,
static_routes::{RegenerationFn, ResolvedStaticPath},
Method, PathSegment, RouteList, RouteListing, SsrMode,
};
use once_cell::sync::Lazy;
use parking_lot::RwLock;
use send_wrapper::SendWrapper;
use server_fn::{
@@ -37,7 +45,9 @@ use server_fn::{
};
use std::{
fmt::{Debug, Display},
future::Future,
ops::{Deref, DerefMut},
path::Path,
sync::Arc,
};
@@ -728,13 +738,25 @@ pub fn render_app_async_with_context<IV>(
where
IV: IntoView + 'static,
{
handle_response(method, additional_context, app_fn, |app, chunks| {
Box::pin(async move {
let app = app.to_html_stream_in_order().collect::<String>().await;
let chunks = chunks();
Box::pin(once(async move { app }).chain(chunks))
as PinnedStream<String>
})
handle_response(method, additional_context, app_fn, async_stream_builder)
}
fn async_stream_builder<IV>(
app: IV,
chunks: BoxedFnOnce<PinnedStream<String>>,
) -> PinnedFuture<PinnedStream<String>>
where
IV: IntoView + 'static,
{
Box::pin(async move {
let app = if cfg!(feature = "islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
};
let app = app.collect::<String>().await;
let chunks = chunks();
Box::pin(once(async move { app }).chain(chunks)) as PinnedStream<String>
})
}
@@ -822,7 +844,7 @@ where
/// create routes in Actix's App without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths.
pub fn generate_route_list<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
app_fn: impl Fn() -> IV + 'static + Send + Clone,
) -> Vec<ActixRouteListing>
where
IV: IntoView + 'static,
@@ -834,8 +856,8 @@ where
/// create routes in Actix's App without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths.
pub fn generate_route_list_with_ssg<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
) -> (Vec<ActixRouteListing>, StaticDataMap)
app_fn: impl Fn() -> IV + 'static + Send + Clone,
) -> (Vec<ActixRouteListing>, StaticRouteGenerator)
where
IV: IntoView + 'static,
{
@@ -847,7 +869,7 @@ where
/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths. Adding excluded_routes
/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Actix path format
pub fn generate_route_list_with_exclusions<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
app_fn: impl Fn() -> IV + 'static + Send + Clone,
excluded_routes: Option<Vec<String>>,
) -> Vec<ActixRouteListing>
where
@@ -861,9 +883,9 @@ where
/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths. Adding excluded_routes
/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Actix path format
pub fn generate_route_list_with_exclusions_and_ssg<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
app_fn: impl Fn() -> IV + 'static + Send + Clone,
excluded_routes: Option<Vec<String>>,
) -> (Vec<ActixRouteListing>, StaticDataMap)
) -> (Vec<ActixRouteListing>, StaticRouteGenerator)
where
IV: IntoView + 'static,
{
@@ -912,7 +934,7 @@ pub struct ActixRouteListing {
path: String,
mode: SsrMode,
methods: Vec<leptos_router::Method>,
static_mode: Option<(StaticMode, StaticDataMap)>,
regenerate: Vec<RegenerationFn>,
}
impl From<RouteListing> for ActixRouteListing {
@@ -925,12 +947,12 @@ impl From<RouteListing> for ActixRouteListing {
};
let mode = value.mode();
let methods = value.methods().collect();
let static_mode = value.into_static_parts();
let regenerate = value.regenerate().into();
Self {
path,
mode,
mode: mode.clone(),
methods,
static_mode,
regenerate,
}
}
}
@@ -941,13 +963,13 @@ impl ActixRouteListing {
path: String,
mode: SsrMode,
methods: impl IntoIterator<Item = leptos_router::Method>,
static_mode: Option<(StaticMode, StaticDataMap)>,
regenerate: impl Into<Vec<RegenerationFn>>,
) -> Self {
Self {
path,
mode,
methods: methods.into_iter().collect(),
static_mode,
regenerate: regenerate.into(),
}
}
@@ -958,19 +980,13 @@ impl ActixRouteListing {
/// The rendering mode for this path.
pub fn mode(&self) -> SsrMode {
self.mode
self.mode.clone()
}
/// The HTTP request methods this path can handle.
pub fn methods(&self) -> impl Iterator<Item = leptos_router::Method> + '_ {
self.methods.iter().copied()
}
/// Whether this route is statically rendered.
#[inline(always)]
pub fn static_mode(&self) -> Option<StaticMode> {
self.static_mode.as_ref().map(|n| n.0)
}
}
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
@@ -979,10 +995,10 @@ impl ActixRouteListing {
/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Actix path format.
/// Additional context will be provided to the app Element.
pub fn generate_route_list_with_exclusions_and_ssg_and_context<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
app_fn: impl Fn() -> IV + 'static + Send + Clone,
excluded_routes: Option<Vec<String>>,
additional_context: impl Fn() + 'static + Clone,
) -> (Vec<ActixRouteListing>, StaticDataMap)
additional_context: impl Fn() + 'static + Send + Clone,
) -> (Vec<ActixRouteListing>, StaticRouteGenerator)
where
IV: IntoView + 'static,
{
@@ -1001,6 +1017,12 @@ where
})
.unwrap_or_default();
let generator = StaticRouteGenerator::new(
&routes,
app_fn.clone(),
additional_context.clone(),
);
// Axum's Router defines Root routes as "/" not ""
let mut routes = routes
.into_inner()
@@ -1014,7 +1036,7 @@ where
"/".to_string(),
Default::default(),
[leptos_router::Method::Get],
None,
vec![],
)]
} else {
// Routes to exclude from auto generation
@@ -1024,192 +1046,251 @@ where
}
routes
},
StaticDataMap::new(), // TODO
//static_data_map,
generator,
)
}
/// Allows generating any prerendered routes.
#[allow(clippy::type_complexity)]
pub struct StaticRouteGenerator(
Box<dyn FnOnce(&LeptosOptions) -> PinnedFuture<()> + Send>,
);
impl StaticRouteGenerator {
fn render_route<IV: IntoView + 'static>(
path: String,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + Clone + Send + 'static,
) -> impl Future<Output = (Owner, String)> {
let (meta_context, meta_output) = ServerMetaContext::new();
let additional_context = {
let add_context = additional_context.clone();
move || {
let mock_req = test::TestRequest::with_uri(&path)
.insert_header(("Accept", "text/html"))
.to_http_request();
let res_options = ResponseOptions::default();
provide_contexts(
Request::new(&mock_req),
&meta_context,
&res_options,
);
add_context();
}
};
let (owner, stream) = leptos_integration_utils::build_response(
app_fn.clone(),
additional_context,
async_stream_builder,
);
let sc = owner.shared_context().unwrap();
async move {
let stream = stream.await;
while let Some(pending) = sc.await_deferred() {
pending.await;
}
let html = meta_output
.inject_meta_context(stream)
.await
.collect::<String>()
.await;
(owner, html)
}
}
/// Creates a new static route generator from the given list of route definitions.
pub fn new<IV>(
routes: &RouteList,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static,
{
Self({
let routes = routes.clone();
Box::new(move |options| {
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
Box::pin(routes.generate_static_files(
move |path: &ResolvedStaticPath| {
Self::render_route(
path.to_string(),
app_fn.clone(),
additional_context.clone(),
)
},
move |path: &ResolvedStaticPath,
owner: &Owner,
html: String| {
let options = options.clone();
let path = path.to_owned();
let response_options = owner.with(use_context);
async move {
write_static_route(
&options,
response_options,
path.as_ref(),
&html,
)
.await
}
},
was_404,
))
})
})
}
/// Generates the routes.
pub async fn generate(self, options: &LeptosOptions) {
(self.0)(options).await
}
}
static STATIC_HEADERS: Lazy<DashMap<String, ResponseOptions>> =
Lazy::new(DashMap::new);
fn was_404(owner: &Owner) -> bool {
let resp = owner.with(|| expect_context::<ResponseOptions>());
let status = resp.0.read().status;
if let Some(status) = status {
return status == StatusCode::NOT_FOUND;
}
false
}
fn static_path(options: &LeptosOptions, path: &str) -> String {
use leptos_integration_utils::static_file_path;
// If the path ends with a trailing slash, we generate the path
// as a directory with a index.html file inside.
if path != "/" && path.ends_with("/") {
static_file_path(options, &format!("{}index", path))
} else {
static_file_path(options, path)
}
}
async fn write_static_route(
options: &LeptosOptions,
response_options: Option<ResponseOptions>,
path: &str,
html: &str,
) -> Result<(), std::io::Error> {
if let Some(options) = response_options {
STATIC_HEADERS.insert(path.to_string(), options);
}
let path = static_path(options, path);
let path = Path::new(&path);
if let Some(path) = path.parent() {
tokio::fs::create_dir_all(path).await?;
}
tokio::fs::write(path, &html).await?;
Ok(())
}
fn handle_static_route<IV>(
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
regenerate: Vec<RegenerationFn>,
) -> Route
where
IV: IntoView + 'static,
{
let handler = move |req: HttpRequest, data: Data<LeptosOptions>| {
Box::pin({
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
let regenerate = regenerate.clone();
async move {
let options = data.into_inner();
let orig_path = req.uri().path();
let path = static_path(&options, orig_path);
let path = Path::new(&path);
let exists = tokio::fs::try_exists(path).await.unwrap_or(false);
let (response_options, html) = if !exists {
let path = ResolvedStaticPath::new(orig_path);
let (owner, html) = path
.build(
move |path: &ResolvedStaticPath| {
StaticRouteGenerator::render_route(
path.to_string(),
app_fn.clone(),
additional_context.clone(),
)
},
move |path: &ResolvedStaticPath,
owner: &Owner,
html: String| {
let options = options.clone();
let path = path.to_owned();
let response_options = owner.with(use_context);
async move {
write_static_route(
&options,
response_options,
path.as_ref(),
&html,
)
.await
}
},
was_404,
regenerate,
)
.await;
(owner.with(use_context::<ResponseOptions>), html)
} else {
let headers =
STATIC_HEADERS.get(orig_path).map(|v| v.clone());
(headers, None)
};
// if html is Some(_), it means that `was_error_response` is true and we're not
// actually going to cache this route, just return it as HTML
//
// this if for thing like 404s, where we do not want to cache an endless series of
// typos (or malicious requests)
let mut res = ActixResponse(match html {
Some(html) => {
HttpResponse::Ok().content_type("text/html").body(html)
}
None => match NamedFile::open(path) {
Ok(res) => res.into_response(&req),
Err(err) => HttpResponse::InternalServerError()
.body(err.to_string()),
},
});
if let Some(options) = response_options {
res.extend_response(&options);
}
res.0
}
})
};
web::get().to(handler)
}
pub enum DataResponse<T> {
Data(T),
Response(actix_web::dev::Response<BoxBody>),
}
// TODO static response
/*
fn handle_static_response<'a, IV>(
path: &'a str,
options: &'a LeptosOptions,
app_fn: &'a (impl Fn() -> IV + Clone + Send + 'static),
additional_context: &'a (impl Fn() + 'static + Clone + Send),
res: StaticResponse,
) -> Pin<Box<dyn Future<Output = HttpResponse<String>> + 'a>>
where
IV: IntoView + 'static,
{
Box::pin(async move {
match res {
StaticResponse::ReturnResponse {
body,
status,
content_type,
} => {
let mut res = HttpResponse::new(match status {
StaticStatusCode::Ok => StatusCode::OK,
StaticStatusCode::NotFound => StatusCode::NOT_FOUND,
StaticStatusCode::InternalServerError => {
StatusCode::INTERNAL_SERVER_ERROR
}
});
if let Some(v) = content_type {
res.headers_mut().insert(
HeaderName::from_static("content-type"),
HeaderValue::from_static(v),
);
}
res.set_body(body)
}
StaticResponse::RenderDynamic => {
handle_static_response(
path,
options,
app_fn,
additional_context,
render_dynamic(
path,
options,
app_fn.clone(),
additional_context.clone(),
)
.await,
)
.await
}
StaticResponse::RenderNotFound => {
handle_static_response(
path,
options,
app_fn,
additional_context,
not_found_page(
tokio::fs::read_to_string(not_found_path(options))
.await,
),
)
.await
}
StaticResponse::WriteFile { body, path } => {
if let Some(path) = path.parent() {
if let Err(e) = std::fs::create_dir_all(path) {
tracing::error!(
"encountered error {} writing directories {}",
e,
path.display()
);
}
}
if let Err(e) = std::fs::write(&path, &body) {
tracing::error!(
"encountered error {} writing file {}",
e,
path.display()
);
}
handle_static_response(
path.to_str().unwrap(),
options,
app_fn,
additional_context,
StaticResponse::ReturnResponse {
body,
status: StaticStatusCode::Ok,
content_type: Some("text/html"),
},
)
.await
}
}
})
}
fn static_route<IV>(
options: LeptosOptions,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + 'static + Clone + Send,
method: Method,
mode: StaticMode,
) -> Route
where
IV: IntoView + 'static,
{
match mode {
StaticMode::Incremental => {
let handler = move |req: HttpRequest| {
Box::pin({
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
async move {
handle_static_response(
req.path(),
&options,
&app_fn,
&additional_context,
incremental_static_route(
tokio::fs::read_to_string(static_file_path(
&options,
req.path(),
))
.await,
),
)
.await
}
})
};
match method {
Method::Get => web::get().to(handler),
Method::Post => web::post().to(handler),
Method::Put => web::put().to(handler),
Method::Delete => web::delete().to(handler),
Method::Patch => web::patch().to(handler),
}
}
StaticMode::Upfront => {
let handler = move |req: HttpRequest| {
Box::pin({
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
async move {
handle_static_response(
req.path(),
&options,
&app_fn,
&additional_context,
upfront_static_route(
tokio::fs::read_to_string(static_file_path(
&options,
req.path(),
))
.await,
),
)
.await
}
})
};
match method {
Method::Get => web::get().to(handler),
Method::Post => web::post().to(handler),
Method::Put => web::put().to(handler),
Method::Delete => web::delete().to(handler),
Method::Patch => web::patch().to(handler),
}
}
}
}
*/
/// This trait allows one to pass a list of routes and a render function to Actix's router, letting us avoid
/// having to use wildcards or manually define all routes in multiple places.
pub trait LeptosRoutes {
@@ -1290,19 +1371,15 @@ where
provide_context(method);
additional_context();
};
router = if let Some(static_mode) = listing.static_mode() {
_ = static_mode;
todo!() /*
router.route(
path,
static_route(
app_fn.clone(),
additional_context_and_method.clone(),
method,
static_mode,
),
)
*/
router = if matches!(listing.mode(), SsrMode::Static(_)) {
router.route(
path,
handle_static_route(
additional_context_and_method.clone(),
app_fn.clone(),
listing.regenerate.clone(),
),
)
} else {
router.route(
path,
@@ -1334,6 +1411,7 @@ where
app_fn.clone(),
method,
),
_ => unreachable!()
},
)
};
@@ -1390,7 +1468,17 @@ impl LeptosRoutes for &mut ServiceConfig {
let mode = listing.mode();
for method in listing.methods() {
router = router.route(
if matches!(listing.mode(), SsrMode::Static(_)) {
router = router.route(
path,
handle_static_route(
additional_context.clone(),
app_fn.clone(),
listing.regenerate.clone(),
),
)
} else {
router = router.route(
path,
match mode {
SsrMode::OutOfOrder => {
@@ -1420,8 +1508,10 @@ impl LeptosRoutes for &mut ServiceConfig {
app_fn.clone(),
method,
),
_ => unreachable!()
},
);
}
}
}

View File

@@ -14,6 +14,7 @@ hydration_context = { workspace = true }
axum = { version = "0.7.5", default-features = false, features = [
"matched-path",
] }
dashmap = "6"
futures = "0.3.30"
http = "1.1"
http-body-util = "0.1.2"
@@ -23,6 +24,7 @@ leptos_macro = { workspace = true, features = ["axum"] }
leptos_meta = { workspace = true, features = ["ssr"] }
leptos_router = { workspace = true, features = ["ssr"] }
leptos_integration_utils = { workspace = true }
once_cell = "1"
parking_lot = "0.12.3"
serde_json = "1.0"
tokio = { version = "1.39", default-features = false }

View File

@@ -32,9 +32,11 @@
//! [`examples`](https://github.com/leptos-rs/leptos/tree/main/examples)
//! directory in the Leptos repository.
#[cfg(feature = "default")]
use axum::http::Uri;
use axum::{
body::{Body, Bytes},
extract::{FromRequestParts, MatchedPath},
extract::{FromRef, FromRequestParts, MatchedPath, State},
http::{
header::{self, HeaderName, HeaderValue, ACCEPT, LOCATION, REFERER},
request::Parts,
@@ -44,10 +46,7 @@ use axum::{
routing::{delete, get, patch, post, put},
};
#[cfg(feature = "default")]
use axum::{
extract::{FromRef, State},
http::Uri,
};
use dashmap::DashMap;
use futures::{stream::once, Future, Stream, StreamExt};
use hydration_context::SsrSharedContext;
use leptos::{
@@ -61,12 +60,20 @@ use leptos_integration_utils::{
BoxedFnOnce, ExtendResponse, PinnedFuture, PinnedStream,
};
use leptos_meta::ServerMetaContext;
#[cfg(feature = "default")]
use leptos_router::static_routes::ResolvedStaticPath;
use leptos_router::{
components::provide_server_redirect, location::RequestUrl, PathSegment,
RouteList, RouteListing, SsrMode, StaticDataMap, StaticMode,
components::provide_server_redirect,
location::RequestUrl,
static_routes::{RegenerationFn, StaticParamsMap},
PathSegment, RouteList, RouteListing, SsrMode,
};
#[cfg(feature = "default")]
use once_cell::sync::Lazy;
use parking_lot::RwLock;
use server_fn::{redirect::REDIRECT_HEADER, ServerFnError};
#[cfg(feature = "default")]
use std::path::Path;
use std::{fmt::Debug, io, pin::Pin, sync::Arc};
#[cfg(feature = "default")]
use tower::ServiceExt;
@@ -236,14 +243,20 @@ pub fn redirect(path: &str) {
);
}
} else {
let msg = "Couldn't retrieve either Parts or ResponseOptions while \
trying to redirect().";
#[cfg(feature = "tracing")]
tracing::warn!("{}", &msg);
{
tracing::warn!(
"Couldn't retrieve either Parts or ResponseOptions while \
trying to redirect()."
);
}
#[cfg(not(feature = "tracing"))]
eprintln!("{}", &msg);
{
eprintln!(
"Couldn't retrieve either Parts or ResponseOptions while \
trying to redirect()."
);
}
}
}
@@ -497,10 +510,11 @@ where
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_route<IV>(
pub fn render_route<S, IV>(
paths: Vec<AxumRouteListing>,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> impl Fn(
State<S>,
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
+ Clone
@@ -508,6 +522,8 @@ pub fn render_route<IV>(
+ 'static
where
IV: IntoView + 'static,
LeptosOptions: FromRef<S>,
S: Send + 'static,
{
render_route_with_context(paths, || {}, app_fn)
}
@@ -648,11 +664,12 @@ where
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_route_with_context<IV>(
pub fn render_route_with_context<S, IV>(
paths: Vec<AxumRouteListing>,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> impl Fn(
State<S>,
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
+ Clone
@@ -660,6 +677,8 @@ pub fn render_route_with_context<IV>(
+ 'static
where
IV: IntoView + 'static,
LeptosOptions: FromRef<S>,
S: Send + 'static,
{
let ooo = render_app_to_stream_with_context(
additional_context.clone(),
@@ -679,7 +698,7 @@ where
app_fn.clone(),
);
move |req| {
move |state, req| {
// 1. Process route to match the values in routeListing
let path = req
.extensions()
@@ -702,6 +721,25 @@ where
SsrMode::PartiallyBlocked => pb(req),
SsrMode::InOrder => io(req),
SsrMode::Async => asyn(req),
SsrMode::Static(_) => {
#[cfg(feature = "default")]
{
let regenerate = listing.regenerate.clone();
handle_static_route(
additional_context.clone(),
app_fn.clone(),
regenerate,
)(state, req)
}
#[cfg(not(feature = "default"))]
{
_ = state;
panic!(
"Static routes are not currently supported on WASM32 \
server targets."
);
}
}
}
}
}
@@ -1097,18 +1135,25 @@ pub fn render_app_async_with_context<IV>(
where
IV: IntoView + 'static,
{
handle_response(additional_context, app_fn, |app, chunks| {
Box::pin(async move {
let app = if cfg!(feature = "islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
};
let app = app.collect::<String>().await;
let chunks = chunks();
Box::pin(once(async move { app }).chain(chunks))
as PinnedStream<String>
})
handle_response(additional_context, app_fn, async_stream_builder)
}
fn async_stream_builder<IV>(
app: IV,
chunks: BoxedFnOnce<PinnedStream<String>>,
) -> PinnedFuture<PinnedStream<String>>
where
IV: IntoView + 'static,
{
Box::pin(async move {
let app = if cfg!(feature = "islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
};
let app = app.collect::<String>().await;
let chunks = chunks();
Box::pin(once(async move { app }).chain(chunks)) as PinnedStream<String>
})
}
@@ -1120,7 +1165,7 @@ where
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn generate_route_list<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
app_fn: impl Fn() -> IV + 'static + Clone + Send,
) -> Vec<AxumRouteListing>
where
IV: IntoView + 'static,
@@ -1128,7 +1173,7 @@ where
generate_route_list_with_exclusions_and_ssg(app_fn, None).0
}
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use t.clone()his to automatically
/// create routes in Axum's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths.
#[cfg_attr(
@@ -1136,8 +1181,8 @@ where
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn generate_route_list_with_ssg<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
) -> (Vec<AxumRouteListing>, StaticDataMap)
app_fn: impl Fn() -> IV + 'static + Clone + Send,
) -> (Vec<AxumRouteListing>, StaticRouteGenerator)
where
IV: IntoView + 'static,
{
@@ -1153,7 +1198,7 @@ where
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn generate_route_list_with_exclusions<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
app_fn: impl Fn() -> IV + 'static + Clone + Send,
excluded_routes: Option<Vec<String>>,
) -> Vec<AxumRouteListing>
where
@@ -1162,13 +1207,13 @@ where
generate_route_list_with_exclusions_and_ssg(app_fn, excluded_routes).0
}
/// TODO docs
/// Builds all routes that have been defined using [`StaticRoute`].
#[allow(unused)]
pub async fn build_static_routes<IV>(
options: &LeptosOptions,
app_fn: impl Fn() -> IV + 'static + Send + Clone,
routes: &[RouteListing],
static_data_map: StaticDataMap,
static_data_map: StaticParamsMap,
) where
IV: IntoView + 'static,
{
@@ -1197,9 +1242,9 @@ pub async fn build_static_routes<IV>(
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn generate_route_list_with_exclusions_and_ssg<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
app_fn: impl Fn() -> IV + 'static + Clone + Send,
excluded_routes: Option<Vec<String>>,
) -> (Vec<AxumRouteListing>, StaticDataMap)
) -> (Vec<AxumRouteListing>, StaticRouteGenerator)
where
IV: IntoView + 'static,
{
@@ -1216,7 +1261,8 @@ pub struct AxumRouteListing {
path: String,
mode: SsrMode,
methods: Vec<leptos_router::Method>,
static_mode: Option<(StaticMode, StaticDataMap)>,
#[allow(unused)]
regenerate: Vec<RegenerationFn>,
}
impl From<RouteListing> for AxumRouteListing {
@@ -1229,12 +1275,12 @@ impl From<RouteListing> for AxumRouteListing {
};
let mode = value.mode();
let methods = value.methods().collect();
let static_mode = value.into_static_parts();
let regenerate = value.regenerate().into();
Self {
path,
mode,
mode: mode.clone(),
methods,
static_mode,
regenerate,
}
}
}
@@ -1245,13 +1291,13 @@ impl AxumRouteListing {
path: String,
mode: SsrMode,
methods: impl IntoIterator<Item = leptos_router::Method>,
static_mode: Option<(StaticMode, StaticDataMap)>,
regenerate: impl Into<Vec<RegenerationFn>>,
) -> Self {
Self {
path,
mode,
methods: methods.into_iter().collect(),
static_mode,
regenerate: regenerate.into(),
}
}
@@ -1261,20 +1307,14 @@ impl AxumRouteListing {
}
/// The rendering mode for this path.
pub fn mode(&self) -> SsrMode {
self.mode
pub fn mode(&self) -> &SsrMode {
&self.mode
}
/// The HTTP request methods this path can handle.
pub fn methods(&self) -> impl Iterator<Item = leptos_router::Method> + '_ {
self.methods.iter().copied()
}
/// Whether this route is statically rendered.
#[inline(always)]
pub fn static_mode(&self) -> Option<StaticMode> {
self.static_mode.as_ref().map(|n| n.0)
}
}
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
@@ -1287,16 +1327,17 @@ impl AxumRouteListing {
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn generate_route_list_with_exclusions_and_ssg_and_context<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
excluded_routes: Option<Vec<String>>,
additional_context: impl Fn() + 'static + Clone,
) -> (Vec<AxumRouteListing>, StaticDataMap)
additional_context: impl Fn() + Clone + Send + 'static,
) -> (Vec<AxumRouteListing>, StaticRouteGenerator)
where
IV: IntoView + 'static,
{
// do some basic reactive setup
init_executor();
let owner = Owner::new_root(Some(Arc::new(SsrSharedContext::new())));
let routes = owner
.with(|| {
// stub out a path for now
@@ -1310,6 +1351,12 @@ where
})
.unwrap_or_default();
let generator = StaticRouteGenerator::new(
&routes,
app_fn.clone(),
additional_context.clone(),
);
// Axum's Router defines Root routes as "/" not ""
let mut routes = routes
.into_inner()
@@ -1323,7 +1370,7 @@ where
"/".to_string(),
Default::default(),
[leptos_router::Method::Get],
None,
vec![],
)]
} else {
// Routes to exclude from auto generation
@@ -1333,16 +1380,284 @@ where
}
routes
},
StaticDataMap::new(), // TODO
//static_data_map,
generator,
)
}
/// Allows generating any prerendered routes.
#[allow(clippy::type_complexity)]
pub struct StaticRouteGenerator(
Box<dyn FnOnce(&LeptosOptions) -> PinnedFuture<()> + Send>,
);
impl StaticRouteGenerator {
#[cfg(feature = "default")]
fn render_route<IV: IntoView + 'static>(
path: String,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + Clone + Send + 'static,
) -> impl Future<Output = (Owner, String)> {
let (meta_context, meta_output) = ServerMetaContext::new();
let additional_context = {
let add_context = additional_context.clone();
move || {
let full_path = format!("http://leptos.dev{path}");
let mock_req = http::Request::builder()
.method(http::Method::GET)
.header("Accept", "text/html")
.body(Body::empty())
.unwrap();
let (mock_parts, _) = mock_req.into_parts();
let res_options = ResponseOptions::default();
provide_contexts(
&full_path,
&meta_context,
mock_parts,
res_options,
);
add_context();
}
};
let (owner, stream) = leptos_integration_utils::build_response(
app_fn.clone(),
additional_context,
async_stream_builder,
);
let sc = owner.shared_context().unwrap();
async move {
let stream = stream.await;
while let Some(pending) = sc.await_deferred() {
pending.await;
}
let html = meta_output
.inject_meta_context(stream)
.await
.collect::<String>()
.await;
(owner, html)
}
}
/// Creates a new static route generator from the given list of route definitions.
pub fn new<IV>(
routes: &RouteList,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static,
{
#[cfg(feature = "default")]
{
Self({
let routes = routes.clone();
Box::new(move |options| {
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
Box::pin(routes.generate_static_files(
move |path: &ResolvedStaticPath| {
Self::render_route(
path.to_string(),
app_fn.clone(),
additional_context.clone(),
)
},
move |path: &ResolvedStaticPath,
owner: &Owner,
html: String| {
let options = options.clone();
let path = path.to_owned();
let response_options = owner.with(use_context);
async move {
write_static_route(
&options,
response_options,
path.as_ref(),
&html,
)
.await
}
},
was_404,
))
})
})
}
#[cfg(not(feature = "default"))]
{
_ = routes;
_ = app_fn;
_ = additional_context;
panic!(
"Static routes are not currently supported on WASM32 server \
targets."
);
}
}
/// Generates the routes.
pub async fn generate(self, options: &LeptosOptions) {
(self.0)(options).await
}
}
#[cfg(feature = "default")]
static STATIC_HEADERS: Lazy<DashMap<String, ResponseOptions>> =
Lazy::new(DashMap::new);
#[cfg(feature = "default")]
fn was_404(owner: &Owner) -> bool {
let resp = owner.with(|| expect_context::<ResponseOptions>());
let status = resp.0.read().status;
if let Some(status) = status {
return status == StatusCode::NOT_FOUND;
}
false
}
#[cfg(feature = "default")]
fn static_path(options: &LeptosOptions, path: &str) -> String {
use leptos_integration_utils::static_file_path;
// If the path ends with a trailing slash, we generate the path
// as a directory with a index.html file inside.
if path != "/" && path.ends_with("/") {
static_file_path(options, &format!("{}index", path))
} else {
static_file_path(options, path)
}
}
#[cfg(feature = "default")]
async fn write_static_route(
options: &LeptosOptions,
response_options: Option<ResponseOptions>,
path: &str,
html: &str,
) -> Result<(), std::io::Error> {
if let Some(options) = response_options {
STATIC_HEADERS.insert(path.to_string(), options);
}
let path = static_path(options, path);
let path = Path::new(&path);
if let Some(path) = path.parent() {
tokio::fs::create_dir_all(path).await?;
}
tokio::fs::write(path, &html).await?;
Ok(())
}
#[cfg(feature = "default")]
fn handle_static_route<S, IV>(
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
regenerate: Vec<RegenerationFn>,
) -> impl Fn(
State<S>,
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
+ Clone
+ Send
+ 'static
where
LeptosOptions: FromRef<S>,
S: Send + 'static,
IV: IntoView + 'static,
{
use tower_http::services::ServeFile;
move |state, req| {
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
let regenerate = regenerate.clone();
Box::pin(async move {
let options = LeptosOptions::from_ref(&state);
let orig_path = req.uri().path();
let path = static_path(&options, orig_path);
let path = Path::new(&path);
let exists = tokio::fs::try_exists(path).await.unwrap_or(false);
let (response_options, html) = if !exists {
let path = ResolvedStaticPath::new(orig_path);
let (owner, html) = path
.build(
move |path: &ResolvedStaticPath| {
StaticRouteGenerator::render_route(
path.to_string(),
app_fn.clone(),
additional_context.clone(),
)
},
move |path: &ResolvedStaticPath,
owner: &Owner,
html: String| {
let options = options.clone();
let path = path.to_owned();
let response_options = owner.with(use_context);
async move {
write_static_route(
&options,
response_options,
path.as_ref(),
&html,
)
.await
}
},
was_404,
regenerate,
)
.await;
(owner.with(use_context::<ResponseOptions>), html)
} else {
let headers = STATIC_HEADERS.get(orig_path).map(|v| v.clone());
(headers, None)
};
// if html is Some(_), it means that `was_error_response` is true and we're not
// actually going to cache this route, just return it as HTML
//
// this if for thing like 404s, where we do not want to cache an endless series of
// typos (or malicious requests)
let mut res = AxumResponse(match html {
Some(html) => axum::response::Html(html).into_response(),
None => match ServeFile::new(path).oneshot(req).await {
Ok(res) => res.into_response(),
Err(err) => (
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {err}"),
)
.into_response(),
},
});
if let Some(options) = response_options {
res.extend_response(&options);
}
res.0
})
}
}
/// This trait allows one to pass a list of routes and a render function to Axum's router, letting us avoid
/// having to use wildcards or manually define all routes in multiple places.
pub trait LeptosRoutes<S>
where
S: Clone + Send + Sync + 'static,
LeptosOptions: FromRef<S>,
{
fn leptos_routes<IV>(
self,
@@ -1372,209 +1687,6 @@ where
H: axum::handler::Handler<T, S>,
T: 'static;
}
/*
#[cfg(feature = "default")]
fn handle_static_response<IV>(
path: String,
options: LeptosOptions,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + Clone + Send + 'static,
res: StaticResponse,
) -> Pin<Box<dyn Future<Output = Response<String>> + 'static>>
where
IV: IntoView + 'static,
{
Box::pin(async move {
match res {
StaticResponse::ReturnResponse {
body,
status,
content_type,
} => {
let mut res = Response::new(body);
if let Some(v) = content_type {
res.headers_mut().insert(
HeaderName::from_static("content-type"),
HeaderValue::from_static(v),
);
}
*res.status_mut() = match status {
StaticStatusCode::Ok => StatusCode::OK,
StaticStatusCode::NotFound => StatusCode::NOT_FOUND,
StaticStatusCode::InternalServerError => {
StatusCode::INTERNAL_SERVER_ERROR
}
};
res
}
StaticResponse::RenderDynamic => {
let res = render_dynamic(
&path,
&options,
app_fn.clone(),
additional_context.clone(),
)
.await;
handle_static_response(
path,
options,
app_fn,
additional_context,
res,
)
.await
}
StaticResponse::RenderNotFound => {
let res = not_found_page(
tokio::fs::read_to_string(not_found_path(&options)).await,
);
handle_static_response(
path,
options,
app_fn,
additional_context,
res,
)
.await
}
StaticResponse::WriteFile { body, path } => {
if let Some(path) = path.parent() {
if let Err(e) = std::fs::create_dir_all(path) {
tracing::error!(
"encountered error {} writing directories {}",
e,
path.display()
);
}
}
if let Err(e) = std::fs::write(&path, &body) {
tracing::error!(
"encountered error {} writing file {}",
e,
path.display()
);
}
handle_static_response(
path.to_str().unwrap().to_string(),
options,
app_fn,
additional_context,
StaticResponse::ReturnResponse {
body,
status: StaticStatusCode::Ok,
content_type: Some("text/html"),
},
)
.await
}
}
})
}*/
#[allow(unused)] // TODO
#[cfg(feature = "default")]
fn static_route<IV, S>(
router: axum::Router<S>,
path: &str,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + Clone + Send + 'static,
method: leptos_router::Method,
mode: StaticMode,
) -> axum::Router<S>
where
IV: IntoView + 'static,
S: Clone + Send + Sync + 'static,
{
todo!()
/*match mode {
StaticMode::Incremental => {
let handler = move |req: Request<Body>| {
Box::pin({
let path = req.uri().path().to_string();
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
async move {
let (tx, rx) = futures::channel::oneshot::channel();
spawn_task!(async move {
let res = incremental_static_route(
tokio::fs::read_to_string(static_file_path(
&options, &path,
))
.await,
);
let res = handle_static_response(
path.clone(),
options,
app_fn,
additional_context,
res,
)
.await;
let _ = tx.send(res);
});
rx.await.expect("to complete HTML rendering")
}
})
};
router.route(
path,
match method {
leptos_router::Method::Get => get(handler),
leptos_router::Method::Post => post(handler),
leptos_router::Method::Put => put(handler),
leptos_router::Method::Delete => delete(handler),
leptos_router::Method::Patch => patch(handler),
},
)
}
StaticMode::Upfront => {
let handler = move |req: Request<Body>| {
Box::pin({
let path = req.uri().path().to_string();
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
async move {
let (tx, rx) = futures::channel::oneshot::channel();
spawn_task!(async move {
let res = upfront_static_route(
tokio::fs::read_to_string(static_file_path(
&options, &path,
))
.await,
);
let res = handle_static_response(
path.clone(),
options,
app_fn,
additional_context,
res,
)
.await;
let _ = tx.send(res);
});
rx.await.expect("to complete HTML rendering")
}
})
};
router.route(
path,
match method {
leptos_router::Method::Get => get(handler),
leptos_router::Method::Post => post(handler),
leptos_router::Method::Put => put(handler),
leptos_router::Method::Delete => delete(handler),
leptos_router::Method::Patch => patch(handler),
},
)
}
}*/
}
trait AxumPath {
fn to_axum_path(&self) -> String;
@@ -1611,6 +1723,7 @@ impl AxumPath for &[PathSegment] {
impl<S> LeptosRoutes<S> for axum::Router<S>
where
S: Clone + Send + Sync + 'static,
LeptosOptions: FromRef<S>,
{
#[cfg_attr(
feature = "tracing",
@@ -1688,25 +1801,24 @@ where
provide_context(method);
cx_with_state();
};
router = if let Some(static_mode) = listing.static_mode() {
router = if matches!(listing.mode(), SsrMode::Static(_)) {
#[cfg(feature = "default")]
{
static_route(
router,
router.route(
path,
app_fn.clone(),
cx_with_state_and_method.clone(),
method,
static_mode,
get(handle_static_route(
cx_with_state_and_method.clone(),
app_fn.clone(),
listing.regenerate.clone(),
)),
)
}
#[cfg(not(feature = "default"))]
{
_ = static_mode;
panic!(
"Static site generation is not currently \
supported on WASM32 server targets."
)
"Static routes are not currently supported on \
WASM32 server targets."
);
}
} else {
router.route(
@@ -1765,6 +1877,7 @@ where
leptos_router::Method::Patch => patch(s),
}
}
_ => unreachable!()
},
)
};

View File

@@ -5,6 +5,7 @@ use leptos::{
reactive_graph::owner::{Owner, Sandboxed},
IntoView,
};
use leptos_config::LeptosOptions;
use leptos_meta::ServerMetaContextOutput;
use std::{future::Future, pin::Pin, sync::Arc};
@@ -132,3 +133,13 @@ where
}));
(owner, stream)
}
pub fn static_file_path(options: &LeptosOptions, path: &str) -> String {
let trimmed_path = path.trim_start_matches('/');
let path = if trimmed_path.is_empty() {
"index"
} else {
trimmed_path
};
format!("{}/{}.html", options.site_root, path)
}

View File

@@ -39,12 +39,36 @@ pub fn HydrationScripts(
options: LeptosOptions,
#[prop(optional)] islands: bool,
) -> impl IntoView {
let pkg_path = &options.site_pkg_dir;
let output_name = &options.output_name;
let mut wasm_output_name = output_name.clone();
if std::option_env!("LEPTOS_OUTPUT_NAME").is_none() {
wasm_output_name.push_str("_bg");
let mut js_file_name = options.output_name.to_string();
let mut wasm_file_name = options.output_name.to_string();
if options.hash_files {
let hash_path = std::env::current_exe()
.map(|path| {
path.parent().map(|p| p.to_path_buf()).unwrap_or_default()
})
.unwrap_or_default()
.join(&options.hash_file);
if hash_path.exists() {
let hashes = std::fs::read_to_string(&hash_path)
.expect("failed to read hash file");
for line in hashes.lines() {
let line = line.trim();
if !line.is_empty() {
if let Some((file, hash)) = line.split_once(':') {
if file == "js" {
js_file_name.push_str(&format!(".{}", hash));
} else if file == "wasm" {
wasm_file_name.push_str(&format!(".{}", hash));
}
}
}
}
}
} else if std::option_env!("LEPTOS_OUTPUT_NAME").is_none() {
wasm_file_name.push_str("_bg");
}
let pkg_path = &options.site_pkg_dir;
#[cfg(feature = "nonce")]
let nonce = crate::nonce::use_nonce();
#[cfg(not(feature = "nonce"))]
@@ -59,16 +83,16 @@ pub fn HydrationScripts(
};
view! {
<link rel="modulepreload" href=format!("/{pkg_path}/{output_name}.js") nonce=nonce.clone()/>
<link rel="modulepreload" href=format!("/{pkg_path}/{js_file_name}.js") nonce=nonce.clone()/>
<link
rel="preload"
href=format!("/{pkg_path}/{wasm_output_name}.wasm")
href=format!("/{pkg_path}/{wasm_file_name}.wasm")
r#as="fetch"
r#type="application/wasm"
crossorigin=nonce.clone().unwrap_or_default()
/>
<script type="module" nonce=nonce>
{format!("{script}({pkg_path:?}, {output_name:?}, {wasm_output_name:?})")}
{format!("{script}({pkg_path:?}, {js_file_name:?}, {wasm_file_name:?})")}
</script>
}
}

View File

@@ -4,13 +4,13 @@
//!
//! Leptos is a full-stack framework for building web applications in Rust. You can use it to build
//! - single-page apps (SPAs) rendered entirely in the browser, using client-side routing and loading
//! or mutating data via async requests to the server
//! or mutating data via async requests to the server.
//! - multi-page apps (MPAs) rendered on the server, managing navigation, data, and mutations via
//! web-standard `<a>` and `<form>` tags
//! web-standard `<a>` and `<form>` tags.
//! - progressively-enhanced single-page apps that are rendered on the server and then hydrated on the client,
//! enhancing your `<a>` and `<form>` navigations and mutations seamlessly when WASM is available.
//!
//! And you can do all three of these **using the same Leptos code.**
//! And you can do all three of these **using the same Leptos code**.
//!
//! Take a look at the [Leptos Book](https://leptos-rs.github.io/leptos/) for a walkthrough of the framework.
//! Join us on our [Discord Channel](https://discord.gg/v38Eef6sWG) to see what the community is building.
@@ -20,76 +20,76 @@
//!
//! If you want to see what Leptos is capable of, check out
//! the [examples](https://github.com/leptos-rs/leptos/tree/main/examples):
//! - [`counter`](https://github.com/leptos-rs/leptos/tree/main/examples/counter) is the classic
//! counter example, showing the basics of client-side rendering and reactive DOM updates
//! - [`counter_without_macros`](https://github.com/leptos-rs/leptos/tree/main/examples/counter_without_macros)
//! adapts the counter example to use the builder pattern for the UI and avoids other macros, instead showing
//! the code that Leptos generates.
//! - [`counters`](https://github.com/leptos-rs/leptos/tree/main/examples/counters) introduces parent-child
//! communication via contexts, and the `<For/>` component for efficient keyed list updates.
//! - [`error_boundary`](https://github.com/leptos-rs/leptos/tree/main/examples/error_boundary) shows how to use
//! `Result` types to handle errors.
//! - [`parent_child`](https://github.com/leptos-rs/leptos/tree/main/examples/parent_child) shows four different
//! ways a parent component can communicate with a child, including passing a closure, context, and more
//! - [`fetch`](https://github.com/leptos-rs/leptos/tree/main/examples/fetch) introduces
//! [Resource]s, which allow you to integrate arbitrary `async` code like an
//!
//! - **[`counter`]** is the classic counter example, showing the basics of client-side rendering and reactive DOM updates.
//! - **[`counter_without_macros`]** adapts the counter example to use the builder pattern for the UI and avoids other macros,
//! instead showing the code that Leptos generates.
//! - **[`counters`]** introduces parent-child communication via contexts, and the [`<For/>`](leptos::prelude::For) component
//! for efficient keyed list updates.
//! - **[`error_boundary`]** shows how to use [`Result`] types to handle errors.
//! - **[`parent_child`]** shows four different ways a parent component can communicate with a child, including passing a closure,
//! context, and more.
//! - **[`fetch`]** introduces [`Resource`](leptos::prelude::Resource)s, which allow you to integrate arbitrary `async` code like an
//! HTTP request within your reactive code.
//! - [`router`](https://github.com/leptos-rs/leptos/tree/main/examples/router) shows how to use Leptoss nested router
//! to enable client-side navigation and route-specific, reactive data loading.
//! - [`slots`](https://github.com/leptos-rs/leptos/tree/main/examples/slots) shows how to use slots on components.
//! - [`spread`](https://github.com/leptos-rs/leptos/tree/main/examples/spread) shows how the spread syntax can be used to spread data and/or event handlers onto elements.
//! - [`counter_isomorphic`](https://github.com/leptos-rs/leptos/tree/main/examples/counter_isomorphic) shows
//! different methods of interaction with a stateful server, including server functions, server actions, forms,
//! and server-sent events (SSE).
//! - [`todomvc`](https://github.com/leptos-rs/leptos/tree/main/examples/todomvc) shows the basics of building an
//! isomorphic web app. Both the server and the client import the same app code from the `todomvc` example.
//! - **[`router`]** shows how to use Leptoss nested router to enable client-side navigation and route-specific, reactive data loading.
//! - **[`slots`]** shows how to use slots on components.
//! - **[`spread`]** shows how the spread syntax can be used to spread data and/or event handlers onto elements.
//! - **[`counter_isomorphic`]** shows different methods of interaction with a stateful server, including server functions,
//! server actions, forms, and server-sent events (SSE).
//! - **[`todomvc`]** shows the basics of building an isomorphic web app. Both the server and the client import the same app code.
//! The server renders the app directly to an HTML string, and the client hydrates that HTML to make it interactive.
//! You might also want to
//! see how we use [`create_effect`] to [serialize JSON to `localStorage`](https://github.com/leptos-rs/leptos/blob/16f084a71268ac325fbc4a5e50c260df185eadb6/examples/todomvc/src/lib.rs#L164)
//! and [reactively call DOM methods](https://github.com/leptos-rs/leptos/blob/6d7c36655c9e7dcc3a3ad33d2b846a3f00e4ae74/examples/todomvc/src/lib.rs#L291)
//! on [references to elements](https://github.com/leptos-rs/leptos/blob/6d7c36655c9e7dcc3a3ad33d2b846a3f00e4ae74/examples/todomvc/src/lib.rs#L254).
//! - [`hackernews`](https://github.com/leptos-rs/leptos/tree/main/examples/hackernews)
//! and [`hackernews_axum`](https://github.com/leptos-rs/leptos/tree/main/examples/hackernews_axum)
//! integrate calls to a real external REST API, routing, server-side rendering and hydration to create
//! a fully-functional application that works as intended even before WASM has loaded and begun to run.
//! - [`todo_app_sqlite`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite) and
//! [`todo_app_sqlite_axum`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite_axum)
//! show how to build a full-stack app using server functions and database connections.
//! - [`tailwind`](https://github.com/leptos-rs/leptos/tree/main/examples/tailwind_csr) shows how to integrate
//! TailwindCSS with `trunk` for CSR.
//! You might also want to see how we use [`Effect::new`](leptos::prelude::Effect::new) to
//! [serialize JSON to `localStorage`](https://github.com/leptos-rs/leptos/blob/20af4928b2fffe017408d3f4e7330db22cf68277/examples/todomvc/src/lib.rs#L191-L209)
//! and [reactively call DOM methods](https://github.com/leptos-rs/leptos/blob/16f084a71268ac325fbc4a5e50c260df185eadb6/examples/todomvc/src/lib.rs#L292-L296)
//! on [references to elements](https://github.com/leptos-rs/leptos/blob/20af4928b2fffe017408d3f4e7330db22cf68277/examples/todomvc/src/lib.rs#L228).
//! - **[`hackernews`]** and **[`hackernews_axum`]** integrate calls to a real external REST API, routing, server-side rendering and
//! hydration to create a fully-functional application that works as intended even before WASM has loaded and begun to run.
//! - **[`todo_app_sqlite`]** and **[`todo_app_sqlite_axum`]** show how to build a full-stack app using server functions and
//! database connections.
//! - **[`tailwind`]** shows how to integrate TailwindCSS with `trunk` for CSR.
//!
//! [`counter`]: https://github.com/leptos-rs/leptos/tree/main/examples/counter
//! [`counter_without_macros`]: https://github.com/leptos-rs/leptos/tree/main/examples/counter_without_macros
//! [`counters`]: https://github.com/leptos-rs/leptos/tree/main/examples/counters
//! [`error_boundary`]: https://github.com/leptos-rs/leptos/tree/main/examples/error_boundary
//! [`parent_child`]: https://github.com/leptos-rs/leptos/tree/main/examples/parent_child
//! [`fetch`]: https://github.com/leptos-rs/leptos/tree/main/examples/fetch
//! [`router`]: https://github.com/leptos-rs/leptos/tree/main/examples/router
//! [`slots`]: https://github.com/leptos-rs/leptos/tree/main/examples/slots
//! [`spread`]: https://github.com/leptos-rs/leptos/tree/main/examples/spread
//! [`counter_isomorphic`]: https://github.com/leptos-rs/leptos/tree/main/examples/counter_isomorphic
//! [`todomvc`]: https://github.com/leptos-rs/leptos/tree/main/examples/todomvc
//! [`hackernews`]: https://github.com/leptos-rs/leptos/tree/main/examples/hackernews
//! [`hackernews_axum`]: https://github.com/leptos-rs/leptos/tree/main/examples/hackernews_axum
//! [`todo_app_sqlite`]: https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite
//! [`todo_app_sqlite_axum`]: https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite_axum
//! [`tailwind`]: https://github.com/leptos-rs/leptos/tree/main/examples/tailwind_csr
//!
//! Details on how to run each example can be found in its README.
//!
//! # Quick Links
//!
//! Here are links to the most important sections of the docs:
//! - **Reactivity**: the [`leptos_reactive`] overview, and more details in
//! - signals: [`create_signal`], [`ReadSignal`], and [`WriteSignal`] (and [`create_rw_signal`] and [`RwSignal`])
//! - computations: [`create_memo`] and [`Memo`]
//! - `async` interop: [`create_resource`] and [`Resource`] for loading data using `async` functions,
//! and [`create_action`] and [`Action`] to mutate data or imperatively call `async` functions.
//! - reactions: [`create_effect`]
//! - **Templating/Views**: the [`view`] macro
//! - **Reactivity**: the [`reactive_graph`] overview, and more details in
//! + signals: [`signal`](leptos::prelude::signal), [`ReadSignal`](leptos::prelude::ReadSignal),
//! [`WriteSignal`](leptos::prelude::WriteSignal) and [`RwSignal`](leptos::prelude::RwSignal).
//! + computations: [`Memo`](leptos::prelude::Memo).
//! + `async` interop: [`Resource`](leptos::prelude::Resource) for loading data using `async` functions
//! and [`Action`](leptos::prelude::Action) to mutate data or imperatively call `async` functions.
//! + reactions: [`Effect`](leptos::prelude::Effect) and [`RenderEffect`](leptos::prelude::RenderEffect).
//! - **Templating/Views**: the [`view`] macro and [`IntoView`](leptos::IntoView) trait.
//! - **Routing**: the [`leptos_router`](https://docs.rs/leptos_router/latest/leptos_router/) crate
//! - **Server Functions**: the [`server`](crate::leptos_server) macro, [`create_action`], and [`create_server_action`]
//! - **Server Functions**: the [`server`](macro@leptos::prelude::server) macro and [`ServerAction`](leptos::prelude::ServerAction).
//!
//! # Feature Flags
//! - `nightly`: On `nightly` Rust, enables the function-call syntax for signal getters and setters.
//! - `csr` Client-side rendering: Generate DOM nodes in the browser
//! - `ssr` Server-side rendering: Generate an HTML string (typically on the server)
//! - `hydrate` Hydration: use this to add interactivity to an SSRed Leptos app
//! - `serde` (*Default*) In SSR/hydrate mode, uses [`serde`](https://docs.rs/serde/latest/serde/) to serialize resources and send them
//!
//! - **`nightly`**: On `nightly` Rust, enables the function-call syntax for signal getters and setters.
//! - **`csr`** Client-side rendering: Generate DOM nodes in the browser.
//! - **`ssr`** Server-side rendering: Generate an HTML string (typically on the server).
//! - **`hydrate`** Hydration: use this to add interactivity to an SSRed Leptos app.
//! - **`rkyv`** In SSR/hydrate mode, uses [`rkyv`](https://docs.rs/rkyv/latest/rkyv/) to serialize resources and send them
//! from the server to the client.
//! - `serde-lite` In SSR/hydrate mode, uses [`serde-lite`](https://docs.rs/serde-lite/latest/serde_lite/) to serialize resources and send them
//! from the server to the client.
//! - `rkyv` In SSR/hydrate mode, uses [`rkyv`](https://docs.rs/rkyv/latest/rkyv/) to serialize resources and send them
//! from the server to the client.
//! - `miniserde` In SSR/hydrate mode, uses [`miniserde`](https://docs.rs/miniserde/latest/miniserde/) to serialize resources and send them
//! from the server to the client.
//! - `tracing` Adds additional support for [`tracing`](https://docs.rs/tracing/latest/tracing/) to components.
//! - `default-tls` Use default native TLS support. (Only applies when using server functions with a non-WASM client like a desktop app.)
//! - `rustls` Use `rustls`. (Only applies when using server functions with a non-WASM client like a desktop app.)
//! - `template_macro` Enables the [`template!`](leptos_macro::template) macro, which offers faster DOM node creation for some use cases in `csr`.
//! - **`tracing`** Adds support for [`tracing`](https://docs.rs/tracing/latest/tracing/).
//!
//! **Important Note:** You must enable one of `csr`, `hydrate`, or `ssr` to tell Leptos
//! which mode your app is operating in. You should only enable one of these per build target,
@@ -103,7 +103,7 @@
//! #[component]
//! pub fn SimpleCounter(initial_value: i32) -> impl IntoView {
//! // create a reactive signal with the initial value
//! let (value, set_value) = signal( initial_value);
//! let (value, set_value) = signal(initial_value);
//!
//! // create event handlers for our buttons
//! // note that `value` and `set_value` are `Copy`, so it's super easy to move them into closures
@@ -112,7 +112,6 @@
//! let increment = move |_| *set_value.write() += 1;
//!
//! view! {
//!
//! <div>
//! <button on:click=clear>"Clear"</button>
//! <button on:click=decrement>"-1"</button>
@@ -124,7 +123,8 @@
//! ```
//!
//! Leptos is easy to use with [Trunk](https://trunkrs.dev/) (or with a simple wasm-bindgen setup):
//! ```
//!
//! ```rust
//! use leptos::{mount::mount_to_body, prelude::*};
//!
//! #[component]
@@ -169,7 +169,7 @@ pub mod prelude {
pub use oco_ref::*;
pub use reactive_graph::{
actions::*, computed::*, effect::*, owner::*, signal::*,
wrappers::read::*, *,
wrappers::read::*,
};
pub use server_fn::{self, ServerFnError};
pub use tachys::{

View File

@@ -13,7 +13,7 @@ use reactive_graph::{
effect::RenderEffect,
owner::{provide_context, use_context, Owner},
signal::ArcRwSignal,
traits::{Dispose, Get, Read, Track, With},
traits::{Get, Read, Track, With},
};
use slotmap::{DefaultKey, SlotMap};
use tachys::{
@@ -264,7 +264,6 @@ where
{
buf.next_id();
let suspense_context = use_context::<SuspenseContext>().unwrap();
let owner = Owner::current().unwrap();
// we need to wait for one of two things: either
@@ -277,7 +276,17 @@ where
futures::channel::oneshot::channel::<()>();
let mut tasks_tx = Some(tasks_tx);
let eff = reactive_graph::effect::Effect::new_isomorphic({
// now, create listener for local resources
let (local_tx, mut local_rx) =
futures::channel::oneshot::channel::<()>();
provide_context(LocalResourceNotifier::from(local_tx));
// walk over the tree of children once to make sure that all resource loads are registered
self.children.dry_resolve();
// check the set of tasks to see if it is empty, now or later
let eff = reactive_graph::effect::RenderEffect::new_isomorphic({
move |_| {
tasks.track();
if tasks.read().is_empty() {
@@ -290,14 +299,6 @@ where
}
});
// now, create listener for local resources
let (local_tx, mut local_rx) =
futures::channel::oneshot::channel::<()>();
provide_context(LocalResourceNotifier::from(local_tx));
// walk over the tree of children once to make sure that all resource loads are registered
self.children.dry_resolve();
let mut fut = Box::pin(ScopedFuture::new(ErrorHookFuture::new(
async move {
// race the local resource notifier against the set of tasks
@@ -337,7 +338,7 @@ where
}
children = children => {
// clean up the (now useless) effect
eff.dispose();
drop(eff);
Some(OwnedView::new_with_owner(children, owner))
}

View File

@@ -20,7 +20,7 @@ syn = { version = "2.0", features = [
"printing",
] }
quote = "1.0"
rstml = "0.11.2"
rstml = "0.12.0"
proc-macro2 = { version = "1.0", features = ["span-locations", "nightly"] }
parking_lot = "0.12.3"
walkdir = "2.5"

View File

@@ -1,4 +1,4 @@
use rstml::node::{NodeElement, NodeName};
use rstml::node::{CustomNode, NodeElement, NodeName};
/// Converts `syn::Block` to simple expression
///
@@ -65,6 +65,6 @@ pub fn is_component_tag_name(name: &NodeName) -> bool {
}
#[must_use]
pub fn is_component_node(node: &NodeElement) -> bool {
pub fn is_component_node(node: &NodeElement<impl CustomNode>) -> bool {
is_component_tag_name(node.name())
}

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_macro"
version = "0.7.0-beta2"
version = "0.7.0-beta4"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
@@ -22,7 +22,7 @@ proc-macro-error = { version = "1.0", default-features = false }
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "2.0", features = ["full"] }
rstml = "0.11.2"
rstml = "0.12.0"
leptos_hot_reload = { workspace = true }
server_fn_macro = { workspace = true }
convert_case = "0.6.0"

View File

@@ -74,6 +74,9 @@ mod slot;
/// Attributes can take a wide variety of primitive types that can be converted to strings. They can also
/// take an `Option`, in which case `Some` sets the attribute and `None` removes the attribute.
///
/// Note that in some cases, rust-analyzer support may be better if attribute values are surrounded with braces (`{}`).
/// Unlike in JSX, attribute values are not required to be in braces, but braces can be used and may improve this LSP support.
///
/// ```rust,ignore
/// # use leptos::prelude::*;
///
@@ -306,10 +309,17 @@ pub fn view(tokens: TokenStream) -> TokenStream {
global_class.as_ref(),
normalized_call_site(proc_macro::Span::call_site()),
);
// The allow lint needs to be put here instead of at the expansion of
// view::attribute_value(). Adding this next to the expanded expression
// seems to break rust-analyzer, but it works when the allow is put here.
quote! {
{
#(#errors;)*
#nodes_output
#[allow(unused_braces)]
{
#(#errors;)*
#nodes_output
}
}
}
.into()

View File

@@ -3,13 +3,14 @@ use crate::view::attribute_absolute;
use proc_macro2::{Ident, TokenStream, TokenTree};
use quote::{format_ident, quote, quote_spanned};
use rstml::node::{
KeyedAttributeValue, NodeAttribute, NodeBlock, NodeElement, NodeName,
CustomNode, KeyedAttributeValue, NodeAttribute, NodeBlock, NodeElement,
NodeName,
};
use std::collections::HashMap;
use syn::{spanned::Spanned, Expr, ExprPath, ExprRange, RangeLimits, Stmt};
pub(crate) fn component_to_tokens(
node: &NodeElement,
node: &NodeElement<impl CustomNode>,
global_class: Option<&TokenTree>,
) -> TokenStream {
let name = node.name();

View File

@@ -10,8 +10,8 @@ use proc_macro2::{Ident, Span, TokenStream, TokenTree};
use proc_macro_error::abort;
use quote::{quote, quote_spanned, ToTokens};
use rstml::node::{
KeyedAttribute, Node, NodeAttribute, NodeBlock, NodeElement, NodeName,
NodeNameFragment,
CustomNode, KVAttributeValue, KeyedAttribute, Node, NodeAttribute,
NodeBlock, NodeElement, NodeName, NodeNameFragment,
};
use std::collections::{HashMap, HashSet};
use syn::{
@@ -89,7 +89,7 @@ pub fn render_view(
}
fn element_children_to_tokens(
nodes: &[Node],
nodes: &[Node<impl CustomNode>],
parent_type: TagType,
parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>,
global_class: Option<&TokenTree>,
@@ -117,7 +117,7 @@ fn element_children_to_tokens(
}
fn fragment_to_tokens(
nodes: &[Node],
nodes: &[Node<impl CustomNode>],
parent_type: TagType,
parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>,
global_class: Option<&TokenTree>,
@@ -142,7 +142,7 @@ fn fragment_to_tokens(
}
fn children_to_tokens(
nodes: &[Node],
nodes: &[Node<impl CustomNode>],
parent_type: TagType,
parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>,
global_class: Option<&TokenTree>,
@@ -186,7 +186,7 @@ fn children_to_tokens(
}
fn node_to_tokens(
node: &Node,
node: &Node<impl CustomNode>,
parent_type: TagType,
parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>,
global_class: Option<&TokenTree>,
@@ -219,6 +219,7 @@ fn node_to_tokens(
global_class,
view_marker,
),
Node::Custom(node) => Some(node.to_token_stream()),
}
}
@@ -236,7 +237,7 @@ fn text_to_tokens(text: &LitStr) -> TokenStream {
}
pub(crate) fn element_to_tokens(
node: &NodeElement,
node: &NodeElement<impl CustomNode>,
mut parent_type: TagType,
parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>,
global_class: Option<&TokenTree>,
@@ -411,7 +412,7 @@ pub(crate) fn element_to_tokens(
}
}
fn is_spread_marker(node: &NodeElement) -> bool {
fn is_spread_marker(node: &NodeElement<impl CustomNode>) -> bool {
match node.name() {
NodeName::Block(block) => matches!(
block.stmts.first(),
@@ -547,10 +548,12 @@ pub(crate) fn attribute_absolute(
node: &KeyedAttribute,
after_spread: bool,
) -> Option<TokenStream> {
let contains_dash = node.key.to_string().contains('-');
let key = node.key.to_string();
let contains_dash = key.contains('-');
let attr_aira = key.starts_with("attr:aria-");
// anything that follows the x:y pattern
match &node.key {
NodeName::Punctuated(parts) if !contains_dash => {
NodeName::Punctuated(parts) if !contains_dash || attr_aira => {
if parts.len() >= 2 {
let id = &parts[0];
match id {
@@ -566,6 +569,14 @@ pub(crate) fn attribute_absolute(
Some(
quote! { ::leptos::tachys::html::#key::#key(#value) },
)
} else if key_name == "aria" {
let mut parts_iter = parts.iter();
parts_iter.next();
let fn_name = parts_iter.map(|p| p.to_string()).collect::<Vec<String>>().join("_");
let key = Ident::new(&fn_name, key.span());
Some(
quote! { ::leptos::tachys::html::attribute::#key(#value) },
)
} else {
Some(
quote! { ::leptos::tachys::html::attribute::#key(#value) },
@@ -763,7 +774,7 @@ fn is_custom_element(tag: &str) -> bool {
tag.contains('-')
}
fn is_self_closing(node: &NodeElement) -> bool {
fn is_self_closing(node: &NodeElement<impl CustomNode>) -> bool {
// self-closing tags
// https://developer.mozilla.org/en-US/docs/Glossary/Empty_element
[
@@ -911,20 +922,31 @@ fn attribute_name(name: &NodeName) -> TokenStream {
}
fn attribute_value(attr: &KeyedAttribute) -> TokenStream {
match attr.value() {
Some(value) => {
if let Expr::Lit(lit) = value {
if cfg!(feature = "nightly") {
if let Lit::Str(str) = &lit.lit {
return quote! {
::leptos::tachys::view::static_types::Static::<#str>
};
match attr.possible_value.to_value() {
None => quote! { true },
Some(value) => match &value.value {
KVAttributeValue::Expr(expr) => {
if let Expr::Lit(lit) = expr {
if cfg!(feature = "nightly") {
if let Lit::Str(str) = &lit.lit {
return quote! {
::leptos::tachys::view::static_types::Static::<#str>
};
}
}
}
quote! {
{#expr}
}
}
quote! { #value }
}
None => quote! { true },
// any value in braces: expand as-is to give proper r-a support
KVAttributeValue::InvalidBraced(block) => {
quote! {
#block
}
}
},
}
}

View File

@@ -2,12 +2,12 @@ use super::{convert_to_snake_case, ident_from_tag_name};
use crate::view::{fragment_to_tokens, TagType};
use proc_macro2::{Ident, TokenStream, TokenTree};
use quote::{format_ident, quote, quote_spanned};
use rstml::node::{KeyedAttribute, NodeAttribute, NodeElement};
use rstml::node::{CustomNode, KeyedAttribute, NodeAttribute, NodeElement};
use std::collections::HashMap;
use syn::spanned::Spanned;
pub(crate) fn slot_to_tokens(
node: &NodeElement,
node: &NodeElement<impl CustomNode>,
slot: &KeyedAttribute,
parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>,
global_class: Option<&TokenTree>,
@@ -213,7 +213,9 @@ pub(crate) fn is_slot(node: &KeyedAttribute) -> bool {
key == "slot" || key.starts_with("slot:")
}
pub(crate) fn get_slot(node: &NodeElement) -> Option<&KeyedAttribute> {
pub(crate) fn get_slot(
node: &NodeElement<impl CustomNode>,
) -> Option<&KeyedAttribute> {
node.attributes().iter().find_map(|node| {
if let NodeAttribute::Attribute(node) = node {
if is_slot(node) {

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_meta"
version = "0.7.0-beta3"
version = "0.7.0-beta4"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"

View File

@@ -1,7 +1,7 @@
use crate::register;
use leptos::{
attr::global::GlobalAttributes, component, tachys::html::element::link,
IntoView,
attr::global::GlobalAttributes, component, prelude::LeptosOptions,
tachys::html::element::link, IntoView,
};
/// Injects an [`HTMLLinkElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement) into the document
@@ -34,3 +34,46 @@ pub fn Stylesheet(
// TODO additional attributes
register(link().id(id).rel("stylesheet").href(href))
}
/// Injects an [`HTMLLinkElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement) into the document head that loads a `cargo-leptos`-hashed stylesheet.
#[component]
pub fn HashedStylesheet(
/// Leptos options
options: LeptosOptions,
/// An ID for the stylesheet.
#[prop(optional, into)]
id: Option<String>,
) -> impl IntoView {
let mut css_file_name = options.output_name.to_string();
if options.hash_files {
let hash_path = std::env::current_exe()
.map(|path| {
path.parent().map(|p| p.to_path_buf()).unwrap_or_default()
})
.unwrap_or_default()
.join(&options.hash_file);
if hash_path.exists() {
let hashes = std::fs::read_to_string(&hash_path)
.expect("failed to read hash file");
for line in hashes.lines() {
let line = line.trim();
if !line.is_empty() {
if let Some((file, hash)) = line.split_once(':') {
if file == "css" {
css_file_name.push_str(&format!(".{}", hash));
}
}
}
}
}
}
css_file_name.push_str(".css");
let pkg_path = &options.site_pkg_dir;
// TODO additional attributes
register(
link()
.id(id)
.rel("stylesheet")
.href(format!("/{pkg_path}/{css_file_name}")),
)
}

View File

@@ -1,6 +1,6 @@
[package]
name = "next_tuple"
version = "0.1.0-beta2"
version = "0.1.0-beta4"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -1,6 +1,6 @@
[package]
name = "reactive_graph"
version = "0.1.0-beta2"
version = "0.1.0-beta4"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -163,10 +163,10 @@ where
#[deprecated = "This function is being removed to conform to Rust idioms. \
Please use `Selector::new()` instead."]
pub fn create_selector<T>(
source: impl Fn() -> T + Clone + Send + Sync + 'static,
source: impl Fn() -> T + Clone + 'static,
) -> Selector<T>
where
T: PartialEq + Eq + Send + Sync + Clone + std::hash::Hash + 'static,
T: PartialEq + Eq + Clone + std::hash::Hash + 'static,
{
Selector::new(source)
}
@@ -178,11 +178,11 @@ where
#[deprecated = "This function is being removed to conform to Rust idioms. \
Please use `Selector::new_with_fn()` instead."]
pub fn create_selector_with_fn<T>(
source: impl Fn() -> T + Clone + Send + Sync + 'static,
source: impl Fn() -> T + Clone + 'static,
f: impl Fn(&T, &T) -> bool + Send + Sync + Clone + 'static,
) -> Selector<T>
where
T: PartialEq + Eq + Send + Sync + Clone + std::hash::Hash + 'static,
T: PartialEq + Eq + Clone + std::hash::Hash + 'static,
{
Selector::new_with_fn(source, f)
}

View File

@@ -30,7 +30,7 @@ use std::{
/// let a = RwSignal::new(0);
/// let is_selected = Selector::new(move || a.get());
/// let total_notifications = StoredValue::new(0);
/// Effect::new_isomorphic({
/// Effect::new({
/// let is_selected = is_selected.clone();
/// move |_| {
/// if is_selected.selected(5) {
@@ -55,7 +55,7 @@ use std::{
///
/// # any_spawner::Executor::tick().await;
/// assert_eq!(is_selected.selected(5), false);
/// # }).await;
/// # });
/// # });
/// ```
#[derive(Clone)]
@@ -74,17 +74,17 @@ where
impl<T> Selector<T>
where
T: PartialEq + Send + Sync + Eq + Clone + Hash + 'static,
T: PartialEq + Eq + Clone + Hash + 'static,
{
/// Creates a new selector that compares values using [`PartialEq`].
pub fn new(source: impl Fn() -> T + Send + Sync + Clone + 'static) -> Self {
pub fn new(source: impl Fn() -> T + Clone + 'static) -> Self {
Self::new_with_fn(source, PartialEq::eq)
}
/// Creates a new selector that compares values by returning `true` from a comparator function
/// if the values are the same.
pub fn new_with_fn(
source: impl Fn() -> T + Clone + Send + Sync + 'static,
source: impl Fn() -> T + Clone + 'static,
f: impl Fn(&T, &T) -> bool + Send + Sync + Clone + 'static,
) -> Self {
let subs: Arc<RwLock<FxHashMap<T, ArcRwSignal<bool>>>> =
@@ -92,7 +92,7 @@ where
let v: Arc<RwLock<Option<T>>> = Default::default();
let f = Arc::new(f) as Arc<dyn Fn(&T, &T) -> bool + Send + Sync>;
let effect = Arc::new(RenderEffect::new_isomorphic({
let effect = Arc::new(RenderEffect::new({
let subs = Arc::clone(&subs);
let f = Arc::clone(&f);
let v = Arc::clone(&v);

View File

@@ -43,7 +43,6 @@ use std::{
/// # use reactive_graph::owner::StoredValue;
/// # tokio_test::block_on(async move {
/// # tokio::task::LocalSet::new().run_until(async move {
/// # any_spawner::Executor::init_tokio();
/// let a = RwSignal::new(0);
/// let b = RwSignal::new(0);
///
@@ -53,9 +52,7 @@ use std::{
/// println!("Value: {}", a.get());
/// });
///
/// # assert_eq!(a.get(), 0);
/// a.set(1);
/// # assert_eq!(a.get(), 1);
/// // ✅ because it's subscribed to `a`, the effect reruns and prints "Value: 1"
///
/// // ❌ don't use effects to synchronize state within the reactive system
@@ -64,7 +61,7 @@ use std::{
/// // and easily lead to problems like infinite loops
/// b.set(a.get() + 1);
/// });
/// # }).await;
/// # });
/// # });
/// ```
/// ## Web-Specific Notes
@@ -185,7 +182,6 @@ impl Effect<LocalStorage> {
/// # use reactive_graph::signal::signal;
/// # tokio_test::block_on(async move {
/// # tokio::task::LocalSet::new().run_until(async move {
/// # any_spawner::Executor::init_tokio();
/// #
/// let (num, set_num) = signal(0);
///
@@ -196,16 +192,13 @@ impl Effect<LocalStorage> {
/// },
/// false,
/// );
/// # assert_eq!(num.get(), 0);
///
/// set_num.set(1); // > "Number: 1; Prev: Some(0)"
/// # assert_eq!(num.get(), 1);
///
/// effect.stop(); // stop watching
///
/// set_num.set(2); // (nothing happens)
/// # assert_eq!(num.get(), 2);
/// # }).await;
/// # });
/// # });
/// ```
///
@@ -217,7 +210,6 @@ impl Effect<LocalStorage> {
/// # use reactive_graph::signal::signal;
/// # tokio_test::block_on(async move {
/// # tokio::task::LocalSet::new().run_until(async move {
/// # any_spawner::Executor::init_tokio();
/// #
/// let (num, set_num) = signal(0);
/// let (cb_num, set_cb_num) = signal(0);
@@ -230,17 +222,12 @@ impl Effect<LocalStorage> {
/// false,
/// );
///
/// # assert_eq!(num.get(), 0);
/// set_num.set(1); // > "Number: 1; Cb: 0"
/// # assert_eq!(num.get(), 1);
///
/// # assert_eq!(cb_num.get(), 0);
/// set_cb_num.set(1); // (nothing happens)
/// # assert_eq!(cb_num.get(), 1);
///
/// set_num.set(2); // > "Number: 2; Cb: 1"
/// # assert_eq!(num.get(), 2);
/// # }).await;
/// # });
/// # });
/// ```
///
@@ -256,7 +243,6 @@ impl Effect<LocalStorage> {
/// # use reactive_graph::signal::signal;
/// # tokio_test::block_on(async move {
/// # tokio::task::LocalSet::new().run_until(async move {
/// # any_spawner::Executor::init_tokio();
/// #
/// let (num, set_num) = signal(0);
///
@@ -268,10 +254,8 @@ impl Effect<LocalStorage> {
/// true,
/// ); // > "Number: 0; Prev: None"
///
/// # assert_eq!(num.get(), 0);
/// set_num.set(1); // > "Number: 1; Prev: Some(0)"
/// # assert_eq!(num.get(), 1);
/// # }).await;
/// # });
/// # });
/// ```
pub fn watch<D, T>(

View File

@@ -135,50 +135,44 @@ where
{
/// Creates a render effect that will run whether the `effects` feature is enabled or not.
pub fn new_isomorphic(
fun: impl FnMut(Option<T>) -> T + Send + Sync + 'static,
mut fun: impl FnMut(Option<T>) -> T + Send + 'static,
) -> Self {
fn erased<T: Send + Sync + 'static>(
mut fun: Box<dyn FnMut(Option<T>) -> T + Send + Sync + 'static>,
) -> RenderEffect<T> {
let (observer, mut rx) = channel();
let value = Arc::new(RwLock::new(None::<T>));
let owner = Owner::new();
let inner = Arc::new(RwLock::new(EffectInner {
dirty: false,
observer,
sources: SourceSet::new(),
}));
let (mut observer, mut rx) = channel();
observer.notify();
let initial_value = owner
.with(|| inner.to_any_subscriber().with_observer(|| fun(None)));
*value.write().or_poisoned() = Some(initial_value);
let value = Arc::new(RwLock::new(None::<T>));
let owner = Owner::new();
let inner = Arc::new(RwLock::new(EffectInner {
dirty: false,
observer,
sources: SourceSet::new(),
}));
let mut first_run = true;
Executor::spawn({
let value = Arc::clone(&value);
let subscriber = inner.to_any_subscriber();
Executor::spawn({
let value = Arc::clone(&value);
let subscriber = inner.to_any_subscriber();
async move {
while rx.next().await.is_some() {
if subscriber
async move {
while rx.next().await.is_some() {
if first_run
|| subscriber
.with_observer(|| subscriber.update_if_necessary())
{
subscriber.clear_sources(&subscriber);
{
first_run = false;
subscriber.clear_sources(&subscriber);
let old_value =
mem::take(&mut *value.write().or_poisoned());
let new_value = owner.with_cleanup(|| {
subscriber.with_observer(|| fun(old_value))
});
*value.write().or_poisoned() = Some(new_value);
}
let old_value =
mem::take(&mut *value.write().or_poisoned());
let new_value = owner.with_cleanup(|| {
subscriber.with_observer(|| fun(old_value))
});
*value.write().or_poisoned() = Some(new_value);
}
}
});
RenderEffect { value, inner }
}
erased(Box::new(fun))
}
});
RenderEffect { value, inner }
}
}

View File

@@ -1,6 +1,9 @@
use crate::owner::Owner;
use or_poisoned::OrPoisoned;
use std::any::{Any, TypeId};
use std::{
any::{Any, TypeId},
collections::VecDeque,
};
impl Owner {
fn provide_context<T: Send + Sync + 'static>(&self, value: T) {
@@ -60,6 +63,35 @@ impl Owner {
None
}
}
/// Searches for items stored in context in either direction, either among parents or among
/// descendants.
pub fn use_context_bidirectional<T: Clone + 'static>(&self) -> Option<T> {
self.use_context()
.unwrap_or_else(|| self.find_context_in_children())
}
fn find_context_in_children<T: Clone + 'static>(&self) -> Option<T> {
let ty = TypeId::of::<T>();
let inner = self.inner.read().or_poisoned();
let mut to_search = VecDeque::new();
to_search.extend(inner.children.clone());
drop(inner);
while let Some(next) = to_search.pop_front() {
if let Some(child) = next.upgrade() {
let child = child.read().or_poisoned();
let contexts = &child.contexts;
if let Some(context) = contexts.get(&ty) {
return context.downcast_ref::<T>().cloned();
}
to_search.extend(child.children.clone());
}
}
None
}
}
/// Provides a context value of type `T` to the current reactive [`Owner`]

View File

@@ -580,7 +580,7 @@ impl<T, S> Dispose for StoredValue<T, S> {
#[inline(always)]
#[track_caller]
#[deprecated(
since = "0.7.0-beta2",
since = "0.7.0-beta4",
note = "This function is being removed to conform to Rust idioms. Please \
use `StoredValue::new()` or `StoredValue::new_local()` instead."
)]

View File

@@ -1,6 +1,6 @@
[package]
name = "reactive_stores"
version = "0.1.0-beta2"
version = "0.1.0-beta4"
rust-version.workspace = true
edition.workspace = true

View File

@@ -1,6 +1,6 @@
[package]
name = "reactive_stores_macro"
version = "0.1.0-beta2"
version = "0.1.0-beta4"
rust-version.workspace = true
edition.workspace = true

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router"
version = "0.7.0-beta2"
version = "0.7.0-beta4"
authors = ["Greg Johnston", "Ben Wishovich"]
license = "MIT"
readme = "../README.md"
@@ -28,6 +28,7 @@ send_wrapper = "0.6.0"
thiserror = "1.0"
percent-encoding = { version = "2.3", optional = true }
gloo-net = "0.6.0"
serde = { version = "1", features = ["derive"] }
[dependencies.web-sys]
version = "0.3.70"

View File

@@ -450,6 +450,12 @@ pub fn Redirect<P>(
"Calling <Redirect/> without a ServerRedirectFunction \
provided, in SSR mode."
);
#[cfg(not(feature = "tracing"))]
eprintln!(
"Calling <Redirect/> without a ServerRedirectFunction \
provided, in SSR mode."
);
return;
}
let navigate = use_navigate();

View File

@@ -2,8 +2,8 @@ use crate::{
location::{LocationProvider, Url},
matching::Routes,
params::ParamsMap,
ChooseView, MatchInterface, MatchNestedRoutes, MatchParams, Method,
PathSegment, RouteList, RouteListing, RouteMatchId,
ChooseView, MatchInterface, MatchNestedRoutes, MatchParams, PathSegment,
RouteList, RouteListing, RouteMatchId,
};
use any_spawner::Executor;
use either_of::{Either, EitherOf3};
@@ -511,10 +511,8 @@ where
RouteListing::new(
path,
data.ssr_mode,
// TODO methods
[Method::Get],
// TODO static data
None,
data.methods,
data.regenerate,
)
})
.collect::<Vec<_>>();

View File

@@ -1,9 +1,17 @@
use crate::{
matching::PathSegment, Method, SsrMode, StaticDataMap, StaticMode,
matching::PathSegment,
static_routes::{
RegenerationFn, ResolvedStaticPath, StaticPath, StaticRoute,
},
Method, SsrMode,
};
use futures::future::join_all;
use reactive_graph::owner::Owner;
use std::{
cell::{Cell, RefCell},
collections::HashSet,
future::Future,
mem,
};
use tachys::{renderer::Renderer, view::RenderHtml};
@@ -13,7 +21,7 @@ pub struct RouteListing {
path: Vec<PathSegment>,
mode: SsrMode,
methods: HashSet<Method>,
static_mode: Option<(StaticMode, StaticDataMap)>,
regenerate: Vec<RegenerationFn>,
}
impl RouteListing {
@@ -22,19 +30,19 @@ impl RouteListing {
path: impl IntoIterator<Item = PathSegment>,
mode: SsrMode,
methods: impl IntoIterator<Item = Method>,
static_mode: Option<(StaticMode, StaticDataMap)>,
regenerate: impl IntoIterator<Item = RegenerationFn>,
) -> Self {
Self {
path: path.into_iter().collect(),
mode,
methods: methods.into_iter().collect(),
static_mode,
regenerate: regenerate.into_iter().collect(),
}
}
/// Create a route listing from a path, with the other fields set to default values.
pub fn from_path(path: impl IntoIterator<Item = PathSegment>) -> Self {
Self::new(path, SsrMode::Async, [], None)
Self::new(path, SsrMode::Async, [], [])
}
/// The path this route handles.
@@ -43,8 +51,8 @@ impl RouteListing {
}
/// The rendering mode for this path.
pub fn mode(&self) -> SsrMode {
self.mode
pub fn mode(&self) -> &SsrMode {
&self.mode
}
/// The HTTP request methods this path can handle.
@@ -52,56 +60,95 @@ impl RouteListing {
self.methods.iter().copied()
}
/// Whether this route is statically rendered.
#[inline(always)]
pub fn static_mode(&self) -> Option<StaticMode> {
self.static_mode.as_ref().map(|n| n.0)
/// The set of regeneration functions that should be applied to this route, if it is statically
/// generated (either up front or incrementally).
pub fn regenerate(&self) -> &[RegenerationFn] {
&self.regenerate
}
/// Whether this route is statically rendered.
#[inline(always)]
pub fn static_data_map(&self) -> Option<&StaticDataMap> {
self.static_mode.as_ref().map(|n| &n.1)
pub fn static_route(&self) -> Option<&StaticRoute> {
match self.mode {
SsrMode::Static(ref route) => Some(route),
_ => None,
}
}
pub fn into_static_parts(self) -> Option<(StaticMode, StaticDataMap)> {
self.static_mode
pub async fn into_static_paths(self) -> Option<Vec<ResolvedStaticPath>> {
let params = self.static_route()?.to_prerendered_params().await;
Some(StaticPath::new(self.path).into_paths(params))
}
pub async fn generate_static_files<Fut, WriterFut>(
mut self,
render_fn: impl Fn(&ResolvedStaticPath) -> Fut + Send + Clone + 'static,
writer: impl Fn(&ResolvedStaticPath, &Owner, String) -> WriterFut
+ Send
+ Clone
+ 'static,
was_404: impl Fn(&Owner) -> bool + Send + Clone + 'static,
) where
Fut: Future<Output = (Owner, String)> + Send + 'static,
WriterFut: Future<Output = Result<(), std::io::Error>> + Send + 'static,
{
if let SsrMode::Static(_) = self.mode() {
let (all_initial_tx, all_initial_rx) = std::sync::mpsc::channel();
let render_fn = render_fn.clone();
let regenerate = mem::take(&mut self.regenerate);
let paths = self.into_static_paths().await.unwrap_or_default();
for path in paths {
// Err(_) here would just mean they've dropped the rx and are no longer awaiting
// it; we're only using it to notify them it's done so it doesn't matter in that
// case
_ = all_initial_tx.send(path.build(
render_fn.clone(),
writer.clone(),
was_404.clone(),
regenerate.clone(),
));
}
join_all(all_initial_rx.try_iter()).await;
}
}
/*
/// Build a route statically, will return `Ok(true)` on success or `Ok(false)` when the route
/// is not marked as statically rendered. All route parameters to use when resolving all paths
/// to render should be passed in the `params` argument.
pub async fn build_static<IV>(
&self,
options: &LeptosOptions,
app_fn: impl Fn() -> IV + Send + 'static + Clone,
additional_context: impl Fn() + Send + 'static + Clone,
params: &StaticParamsMap,
) -> Result<bool, std::io::Error>
where
IV: IntoView + 'static,
{
match self.static_mode {
None => Ok(false),
Some(_) => {
let mut path = StaticPath::new(&self.leptos_path);
path.add_params(params);
for path in path.into_paths() {
path.write(
options,
app_fn.clone(),
additional_context.clone(),
)
.await?;
/// Build a route statically, will return `Ok(true)` on success or `Ok(false)` when the route
/// is not marked as statically rendered. All route parameters to use when resolving all paths
/// to render should be passed in the `params` argument.
pub async fn build_static<IV>(
&self,
options: &LeptosOptions,
app_fn: impl Fn() -> IV + Send + 'static + Clone,
additional_context: impl Fn() + Send + 'static + Clone,
params: &StaticParamsMap,
) -> Result<bool, std::io::Error>
where
IV: IntoView + 'static,
{
match self.mode {
SsrMode::Static(route) => {
let mut path = StaticPath::new(self.path.clone());
for path in path.into_paths(params) {
/*path.write(
options,
app_fn.clone(),
additional_context.clone(),
)
.await?;*/ println!()
}
Ok(true)
}
Ok(true)
_ => Ok(false),
}
}
}*/
*/
}
#[derive(Debug, Default)]
#[derive(Debug, Default, Clone)]
pub struct RouteList(Vec<RouteListing>);
impl From<Vec<RouteListing>> for RouteList {
@@ -124,6 +171,45 @@ impl RouteList {
pub fn into_inner(self) -> Vec<RouteListing> {
self.0
}
pub fn iter(&self) -> impl Iterator<Item = &RouteListing> {
self.0.iter()
}
pub async fn into_static_paths(self) -> Vec<ResolvedStaticPath> {
futures::future::join_all(
self.into_inner()
.into_iter()
.map(|route_listing| route_listing.into_static_paths()),
)
.await
.into_iter()
.flatten()
.flatten()
.collect::<Vec<_>>()
}
pub async fn generate_static_files<Fut, WriterFut>(
self,
render_fn: impl Fn(&ResolvedStaticPath) -> Fut + Send + Clone + 'static,
writer: impl Fn(&ResolvedStaticPath, &Owner, String) -> WriterFut
+ Send
+ Clone
+ 'static,
was_404: impl Fn(&Owner) -> bool + Send + Clone + 'static,
) where
Fut: Future<Output = (Owner, String)> + Send + 'static,
WriterFut: Future<Output = Result<(), std::io::Error>> + Send + 'static,
{
join_all(self.into_inner().into_iter().map(|route| {
route.generate_static_files(
render_fn.clone(),
writer.clone(),
was_404.clone(),
)
}))
.await;
}
}
impl RouteList {

View File

@@ -155,8 +155,10 @@ pub fn use_location() -> Location {
location
}
pub(crate) type RawParamsMap = ArcMemo<ParamsMap>;
#[track_caller]
fn use_params_raw() -> ArcMemo<ParamsMap> {
fn use_params_raw() -> RawParamsMap {
use_context().expect(
"Tried to access params outside the context of a matched <Route>.",
)

View File

@@ -16,7 +16,7 @@ pub mod nested_router;
pub mod params;
//mod router;
mod ssr_mode;
mod static_route;
pub mod static_routes;
pub use generate_route_list::*;
#[doc(inline)]
@@ -26,4 +26,3 @@ pub use method::*;
pub use navigate::*;
//pub use router::*;
pub use ssr_mode::*;
pub use static_route::*;

View File

@@ -201,10 +201,29 @@ where
}
}
#[cfg(feature = "ssr")]
pub(crate) fn unescape(s: &str) -> String {
percent_encoding::percent_decode_str(s)
.decode_utf8()
.unwrap()
.to_string()
}
#[cfg(not(feature = "ssr"))]
pub(crate) fn unescape(s: &str) -> String {
js_sys::decode_uri_component(s).unwrap().into()
}
#[cfg(not(feature = "ssr"))]
pub(crate) fn unescape_minimal(s: &str) -> String {
js_sys::decode_uri(s).unwrap().into()
}
#[cfg(feature = "ssr")]
pub(crate) fn unescape_minimal(s: &str) -> String {
unescape(s)
}
pub(crate) fn handle_anchor_click<NavFn, NavFut>(
router_base: Option<Cow<'static, str>>,
parse_with_base: fn(&str, &str) -> Result<Url, JsValue>,
@@ -259,7 +278,7 @@ where
}
let url = parse_with_base(href.as_str(), &origin).unwrap();
let path_name = unescape(&url.path);
let path_name = unescape_minimal(&url.path);
// let browser handle this event if it leaves our domain
// or our base path

View File

@@ -6,10 +6,10 @@ pub use path_segment::*;
mod horizontal;
mod nested;
mod vertical;
use crate::SsrMode;
use crate::{static_routes::RegenerationFn, Method, SsrMode};
pub use horizontal::*;
pub use nested::*;
use std::{borrow::Cow, marker::PhantomData};
use std::{borrow::Cow, collections::HashSet, marker::PhantomData};
use tachys::{
renderer::Renderer,
view::{Render, RenderHtml},
@@ -145,6 +145,8 @@ where
pub struct GeneratedRouteData {
pub segments: Vec<PathSegment>,
pub ssr_mode: SsrMode,
pub methods: HashSet<Method>,
pub regenerate: Vec<RegenerationFn>,
}
#[cfg(test)]

View File

@@ -2,11 +2,12 @@ use super::{
MatchInterface, MatchNestedRoutes, PartialPathMatch, PathSegment,
PossibleRouteMatch, RouteMatchId,
};
use crate::{ChooseView, GeneratedRouteData, MatchParams, SsrMode};
use crate::{ChooseView, GeneratedRouteData, MatchParams, Method, SsrMode};
use core::{fmt, iter};
use either_of::Either;
use std::{
borrow::Cow,
collections::HashSet,
marker::PhantomData,
sync::atomic::{AtomicU16, Ordering},
};
@@ -19,7 +20,7 @@ mod tuples;
static ROUTE_ID: AtomicU16 = AtomicU16::new(1);
#[derive(Debug, Copy, PartialEq, Eq)]
#[derive(Debug, PartialEq, Eq)]
pub struct NestedRoute<Segments, Children, Data, View, R> {
id: u16,
segments: Segments,
@@ -27,6 +28,7 @@ pub struct NestedRoute<Segments, Children, Data, View, R> {
data: Data,
view: View,
rndr: PhantomData<R>,
methods: HashSet<Method>,
ssr_mode: SsrMode,
}
@@ -46,7 +48,8 @@ where
data: self.data.clone(),
view: self.view.clone(),
rndr: PhantomData,
ssr_mode: self.ssr_mode,
methods: self.methods.clone(),
ssr_mode: self.ssr_mode.clone(),
}
}
}
@@ -64,6 +67,7 @@ impl<Segments, View, R> NestedRoute<Segments, (), (), View, R> {
data: (),
view,
rndr: PhantomData,
methods: [Method::Get].into(),
ssr_mode: Default::default(),
}
}
@@ -81,6 +85,7 @@ impl<Segments, Data, View, R> NestedRoute<Segments, (), Data, View, R> {
view,
rndr,
ssr_mode,
methods,
..
} = self;
NestedRoute {
@@ -90,6 +95,7 @@ impl<Segments, Data, View, R> NestedRoute<Segments, (), Data, View, R> {
data,
view,
ssr_mode,
methods,
rndr,
}
}
@@ -249,25 +255,44 @@ where
let mut segment_routes = Vec::new();
self.segments.generate_path(&mut segment_routes);
let children = self.children.as_ref();
let ssr_mode = self.ssr_mode;
let ssr_mode = self.ssr_mode.clone();
let methods = self.methods.clone();
let regenerate = match &ssr_mode {
SsrMode::Static(data) => match data.regenerate.as_ref() {
None => vec![],
Some(regenerate) => vec![regenerate.clone()]
}
_ => vec![]
};
match children {
None => Either::Left(iter::once(GeneratedRouteData {
segments: segment_routes,
ssr_mode
ssr_mode,
methods,
regenerate
})),
Some(children) => {
Either::Right(children.generate_routes().into_iter().map(move |child| {
// extend this route's segments with child segments
let segments = segment_routes.clone().into_iter().chain(child.segments).collect();
let mut methods = methods.clone();
methods.extend(child.methods);
let mut regenerate = regenerate.clone();
regenerate.extend(child.regenerate);
if child.ssr_mode > ssr_mode {
GeneratedRouteData {
segments,
ssr_mode: child.ssr_mode,
methods, regenerate
}
} else {
GeneratedRouteData {
segments,
ssr_mode,
ssr_mode: ssr_mode.clone(), methods, regenerate
}
}
}))

View File

@@ -3,8 +3,8 @@ use crate::{
location::{LocationProvider, Url},
matching::Routes,
params::ParamsMap,
ChooseView, MatchInterface, MatchNestedRoutes, MatchParams, Method,
PathSegment, RouteList, RouteListing, RouteMatchId,
ChooseView, MatchInterface, MatchNestedRoutes, MatchParams, PathSegment,
RouteList, RouteListing, RouteMatchId,
};
use any_spawner::Executor;
use either_of::{Either, EitherOf3};
@@ -272,10 +272,8 @@ where
RouteListing::new(
path,
data.ssr_mode,
// TODO methods
[Method::Get],
// TODO static data
None,
data.methods,
data.regenerate,
)
})
.collect::<Vec<_>>();

View File

@@ -1,3 +1,5 @@
use crate::static_routes::StaticRoute;
/// Indicates which rendering mode should be used for this route during server-side rendering.
///
/// Leptos supports the following ways of rendering HTML that contains `async` data loaded
@@ -18,15 +20,17 @@
/// 5. **`Async`**: Load all resources on the server. Wait until all data are loaded, and render HTML in one sweep.
/// - *Pros*: Better handling for meta tags (because you know async data even before you render the `<head>`). Faster complete load than **synchronous** because async resources begin loading on server.
/// - *Cons*: Slower load time/TTFB: you need to wait for all async resources to load before displaying anything on the client.
/// 6. **`Static`**:
///
/// The mode defaults to out-of-order streaming. For a path that includes multiple nested routes, the most
/// restrictive mode will be used: i.e., if even a single nested route asks for `Async` rendering, the whole initial
/// request will be rendered `Async`. (`Async` is the most restricted requirement, followed by `InOrder`, `PartiallyBlocked`, and `OutOfOrder`.)
#[derive(Default, Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[derive(Default, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum SsrMode {
#[default]
OutOfOrder,
PartiallyBlocked,
InOrder,
Async,
Static(StaticRoute),
}

View File

@@ -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
View 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()
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router_macro"
version = "0.7.0-beta2"
version = "0.7.0-beta4"
authors = ["Greg Johnston", "Ben Wishovich"]
license = "MIT"
readme = "../README.md"
@@ -18,4 +18,4 @@ proc-macro2 = "1.0"
quote = "1.0"
[dev-dependencies]
leptos_router = { workspace = true }
leptos_router = { version = "0.7.0-beta" }

View File

@@ -75,6 +75,9 @@ impl SegmentParser {
lit.trim_start_matches(['"', '/'])
.trim_end_matches(['"', '/']),
);
if lit.ends_with(r#"/""#) && lit != r#""/""# {
self.segments.push(Segment::Static("/".to_string()));
}
}
TokenTree::Group(_) => unimplemented!(),
TokenTree::Ident(_) => unimplemented!(),
@@ -102,13 +105,14 @@ impl SegmentParser {
impl Segment {
fn is_valid(segment: &str) -> bool {
segment.chars().all(|c| {
c.is_ascii_digit()
|| c.is_ascii_lowercase()
|| c.is_ascii_uppercase()
|| RFC3986_UNRESERVED.contains(&c)
|| RFC3986_PCHAR_OTHER.contains(&c)
})
segment == "/"
|| segment.chars().all(|c| {
c.is_ascii_digit()
|| c.is_ascii_lowercase()
|| c.is_ascii_uppercase()
|| RFC3986_UNRESERVED.contains(&c)
|| RFC3986_PCHAR_OTHER.contains(&c)
})
}
fn ensure_valid(&self) {

View File

@@ -64,14 +64,14 @@ fn parses_no_slashes() {
#[test]
fn parses_no_leading_slash() {
let output = path!("home/");
let output = path!("home");
assert_eq!(output, (StaticSegment("home"),));
}
#[test]
fn parses_trailing_slash() {
let output = path!("/home/");
assert_eq!(output, (StaticSegment("home"),));
assert_eq!(output, (StaticSegment("home"), StaticSegment("/")));
}
#[test]
@@ -105,6 +105,19 @@ fn parses_mixed_segment_types() {
);
}
#[test]
fn parses_trailing_slash_after_param() {
let output = path!("/foo/:bar/");
assert_eq!(
output,
(
StaticSegment("foo"),
ParamSegment("bar"),
StaticSegment("/")
)
);
}
#[test]
fn parses_consecutive_static() {
let output = path!("/foo/bar/baz");

View File

@@ -8,7 +8,7 @@
use convert_case::{Case, Converter};
use proc_macro2::{Literal, Span, TokenStream as TokenStream2};
use quote::{quote, quote_spanned, ToTokens};
use quote::{format_ident, quote, quote_spanned, ToTokens};
use syn::{
parse::{Parse, ParseStream},
punctuated::Punctuated,
@@ -74,32 +74,117 @@ pub fn server_macro_impl(
ident.mutability = None;
}
// allow #[server(default)] on fields
let mut default = false;
let mut other_attrs = Vec::new();
for attr in typed_arg.attrs.iter() {
if !attr.path().is_ident("server") {
other_attrs.push(attr.clone());
continue;
fn rename_path(
path: Path,
from_ident: Ident,
to_ident: Ident,
) -> Path {
if path.is_ident(&from_ident) {
Path {
leading_colon: None,
segments: Punctuated::from_iter([PathSegment {
ident: to_ident,
arguments: PathArguments::None,
}]),
}
} else {
path
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("default") && meta.input.is_empty() {
default = true;
Ok(())
}
let attrs = typed_arg
.attrs
.iter()
.cloned()
.map(|attr| {
if attr.path().is_ident("server") {
// Allow the following attributes:
// - #[server(default)]
// - #[server(rename = "fieldName")]
// Rename `server` to `serde`
let attr = Attribute {
meta: match attr.meta {
Meta::Path(path) => Meta::Path(rename_path(
path,
format_ident!("server"),
format_ident!("serde"),
)),
Meta::List(mut list) => {
list.path = rename_path(
list.path,
format_ident!("server"),
format_ident!("serde"),
);
Meta::List(list)
}
Meta::NameValue(mut name_value) => {
name_value.path = rename_path(
name_value.path,
format_ident!("server"),
format_ident!("serde"),
);
Meta::NameValue(name_value)
}
},
..attr
};
let args = attr.parse_args::<Meta>()?;
match args {
// #[server(default)]
Meta::Path(path) if path.is_ident("default") => {
Ok(attr.clone())
}
// #[server(flatten)]
Meta::Path(path) if path.is_ident("flatten") => {
Ok(attr.clone())
}
// #[server(default = "value")]
Meta::NameValue(name_value)
if name_value.path.is_ident("default") =>
{
Ok(attr.clone())
}
// #[server(skip)]
Meta::Path(path) if path.is_ident("skip") => {
Ok(attr.clone())
}
// #[server(rename = "value")]
Meta::NameValue(name_value)
if name_value.path.is_ident("rename") =>
{
Ok(attr.clone())
}
_ => Err(Error::new(
attr.span(),
"Unrecognized #[server] attribute, expected \
#[server(default)] or #[server(rename = \
\"fieldName\")]",
)),
}
} else if attr.path().is_ident("doc") {
// Allow #[doc = "documentation"]
Ok(attr.clone())
} else if attr.path().is_ident("allow") {
// Allow #[allow(...)]
Ok(attr.clone())
} else if attr.path().is_ident("deny") {
// Allow #[deny(...)]
Ok(attr.clone())
} else if attr.path().is_ident("ignore") {
// Allow #[ignore]
Ok(attr.clone())
} else {
Err(meta.error(
"Unrecognized #[server] attribute, expected \
#[server(default)]",
Err(Error::new(
attr.span(),
"Unrecognized attribute, expected #[server(...)]",
))
}
})?;
}
typed_arg.attrs = other_attrs;
if default {
Ok(quote! { #[serde(default)] pub #typed_arg })
} else {
Ok(quote! { pub #typed_arg })
}
})
.collect::<Result<Vec<_>>>()?;
typed_arg.attrs = vec![];
Ok(quote! { #(#attrs ) * pub #typed_arg })
})
.collect::<Result<Vec<_>>>()?;

View File

@@ -1,6 +1,6 @@
[package]
name = "tachys"
version = "0.1.0-beta3"
version = "0.1.0-beta4"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -166,7 +166,9 @@ where
self.0.take();
}
async fn resolve(self) -> Self::AsyncOutput {
async fn resolve(mut self) -> Self::AsyncOutput {
// see note on dry_resolve() re: SendWrapper
self.0 = None;
self
}
}

View File

@@ -26,7 +26,7 @@ pub use elements::*;
pub use inner_html::*;
/// The typed representation of an HTML element.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq, Eq)]
pub struct HtmlElement<E, At, Ch, Rndr> {
pub(crate) tag: E,
pub(crate) rndr: PhantomData<Rndr>,
@@ -36,6 +36,23 @@ pub struct HtmlElement<E, At, Ch, Rndr> {
pub(crate) defined_at: &'static std::panic::Location<'static>,
}
impl<E: Clone, At: Clone, Ch: Clone, Rndr> Clone
for HtmlElement<E, At, Ch, Rndr>
{
fn clone(&self) -> Self {
HtmlElement {
tag: self.tag.clone(),
rndr: PhantomData,
attributes: self.attributes.clone(),
children: self.children.clone(),
#[cfg(debug_assertions)]
defined_at: self.defined_at,
}
}
}
impl<E: Copy, At: Copy, Ch: Copy, Rndr> Copy for HtmlElement<E, At, Ch, Rndr> {}
/*impl<E, At, Ch, Rndr> ElementType for HtmlElement<E, At, Ch, Rndr>
where
E: ElementType,

View File

@@ -288,7 +288,9 @@ where
self.cb.take();
}
async fn resolve(self) -> Self::AsyncOutput {
async fn resolve(mut self) -> Self::AsyncOutput {
// see note on dry_resolve() re: SendWrapper
self.cb = None;
self
}
}

View File

@@ -121,7 +121,9 @@ where
self.value.take();
}
async fn resolve(self) -> Self::AsyncOutput {
async fn resolve(mut self) -> Self::AsyncOutput {
// see note on dry_resolve() re: SendWrapper
self.value = None;
self
}
}

View File

@@ -15,7 +15,7 @@ use web_sys::{
};
/// A [`Renderer`] that uses `web-sys` to manipulate DOM elements in the browser.
#[derive(Debug)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Dom;
thread_local! {