Compare commits

..

1 Commits

Author SHA1 Message Date
Greg Johnston
a10ab613b8 fix: support complex punctuated attribute keys in attr: syntax (closes #3221) 2024-11-08 15:53:08 -05:00
277 changed files with 4287 additions and 17327 deletions

12
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "cargo"
directories:
- "/"
schedule:
interval: "daily"
open-pull-requests-limit: 10

View File

@@ -1,54 +0,0 @@
name: autofix.ci
on:
pull_request:
# Running this workflow on main branch pushes requires write permission to apply changes.
# Leave it alone for future uses.
# push:
# branches: ["main"]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
DEBIAN_FRONTEND: noninteractive
jobs:
autofix:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: actions-rust-lang/setup-rust-toolchain@v1
with: {toolchain: nightly, components: "rustfmt, clippy", target: "wasm32-unknown-unknown", rustflags: ""}
- name: Install Glib
run: |
sudo apt-get update
sudo apt-get install -y libglib2.0-dev
- name: Install jq
run: sudo apt-get install jq
- run: |
echo "Formatting the workspace"
cargo fmt --all
echo "Running Clippy against each member's features (default features included)"
for member in $(cargo metadata --no-deps --format-version 1 | jq -r '.packages[] | .name'); do
echo "Working on member $member":
echo -e "\tdefault-features/no-features:"
# this will also run on members with no features or default features
cargo clippy --allow-dirty --fix --lib --package "$member"
features=$(cargo metadata --no-deps --format-version 1 | jq -r ".packages[] | select(.name == \"$member\") | .features | keys[]")
for feature in $features; do
if [ "$feature" = "default" ]; then
continue
fi
echo -e "\tfeature $feature"
cargo clippy --allow-dirty --fix --lib --package "$member" --features "$feature"
done
done
- uses: autofix-ci/action@v1.3.1
if: ${{ always() }}
with:
fail-fast: false

View File

@@ -4,12 +4,10 @@ on:
branches:
- main
- leptos_0.6
- leptos_0.8
pull_request:
branches:
- main
- leptos_0.6
- leptos_0.8
jobs:
get-example-changed:
uses: ./.github/workflows/get-example-changed.yml

View File

@@ -4,12 +4,10 @@ on:
branches:
- main
- leptos_0.6
- leptos_0.8
pull_request:
branches:
- main
- leptos_0.6
- leptos_0.8
jobs:
get-leptos-changed:
uses: ./.github/workflows/get-leptos-changed.yml

View File

@@ -4,27 +4,19 @@ on:
branches:
- main
- leptos_0.6
- leptos_0.8
pull_request:
branches:
- main
- leptos_0.6
- leptos_0.8
env:
DEBIAN_FRONTEND: noninteractive
jobs:
get-leptos-changed:
uses: ./.github/workflows/get-leptos-changed.yml
test:
needs: [get-leptos-changed]
if: needs.get-leptos-changed.outputs.leptos_changed == 'true' && github.event.pull_request.labels[0].name != 'breaking'
if: github.event.pull_request.labels[0].name == 'semver' # needs.get-leptos-changed.outputs.leptos_changed == 'true' && github.event.pull_request.labels[0].name != 'breaking'
name: Run semver check (nightly-2024-08-01)
runs-on: ubuntu-latest
steps:
- name: Install Glib
run: |
sudo apt-get update
sudo apt-get install -y libglib2.0-dev
- name: Checkout
uses: actions/checkout@v4
- name: Semver Checks

View File

@@ -4,12 +4,10 @@ on:
branches:
- main
- leptos_0.6
- leptos_0.8
pull_request:
branches:
- main
- leptos_0.6
- leptos_0.8
jobs:
get-leptos-changed:
uses: ./.github/workflows/get-leptos-changed.yml

View File

@@ -14,7 +14,6 @@ on:
env:
CARGO_TERM_COLOR: always
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
DEBIAN_FRONTEND: noninteractive
jobs:
test:
name: Run ${{ inputs.cargo_make_task }} (${{ inputs.toolchain }})
@@ -34,10 +33,6 @@ jobs:
echo "Disk space after cleanup:"
df -h
# Setup environment
- name: Install Glib
run: |
sudo apt-get update
sudo apt-get install -y libglib2.0-dev
- uses: actions/checkout@v4
- name: Setup Rust
uses: dtolnay/rust-toolchain@master

1
.gitignore vendored
View File

@@ -14,4 +14,3 @@ blob.rs
.vscode
vendor
hash.txt

1068
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -40,36 +40,36 @@ members = [
exclude = ["benchmarks", "examples", "projects"]
[workspace.package]
version = "0.8.0-alpha"
version = "0.7.0-rc1"
edition = "2021"
rust-version = "1.76"
[workspace.dependencies]
throw_error = { path = "./any_error/", version = "0.2.0" }
any_spawner = { path = "./any_spawner/", version = "0.2.0" }
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1" }
throw_error = { path = "./any_error/", version = "0.2.0-rc1" }
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" }
leptos = { path = "./leptos", version = "0.8.0-alpha" }
leptos_config = { path = "./leptos_config", version = "0.8.0-alpha" }
leptos_dom = { path = "./leptos_dom", version = "0.8.0-alpha" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.0-alpha" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.8.0-alpha" }
leptos_macro = { path = "./leptos_macro", version = "0.8.0-alpha" }
leptos_router = { path = "./router", version = "0.8.0-alpha" }
leptos_router_macro = { path = "./router_macro", version = "0.8.0-alpha" }
leptos_server = { path = "./leptos_server", version = "0.8.0-alpha" }
leptos_meta = { path = "./meta", version = "0.8.0-alpha" }
next_tuple = { path = "./next_tuple", version = "0.1.0" }
hydration_context = { path = "./hydration_context", version = "0.2.0-rc1" }
leptos = { path = "./leptos", version = "0.7.0-rc1" }
leptos_config = { path = "./leptos_config", version = "0.7.0-rc1" }
leptos_dom = { path = "./leptos_dom", version = "0.7.0-rc1" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.0-rc1" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.0-rc1" }
leptos_macro = { path = "./leptos_macro", version = "0.7.0-rc1" }
leptos_router = { path = "./router", version = "0.7.0-rc1" }
leptos_router_macro = { path = "./router_macro", version = "0.7.0-rc1" }
leptos_server = { path = "./leptos_server", version = "0.7.0-rc1" }
leptos_meta = { path = "./meta", version = "0.7.0-rc1" }
next_tuple = { path = "./next_tuple", version = "0.1.0-rc1" }
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.4" }
reactive_stores = { path = "./reactive_stores", version = "0.1.3" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0" }
server_fn = { path = "./server_fn", version = "0.8.0-alpha" }
server_fn_macro = { path = "./server_fn_macro", version = "0.8.0-alpha" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.0-alpha" }
tachys = { path = "./tachys", version = "0.1.4" }
reactive_graph = { path = "./reactive_graph", version = "0.1.0-rc1" }
reactive_stores = { path = "./reactive_stores", version = "0.1.0-rc1" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0-rc1" }
server_fn = { path = "./server_fn", version = "0.7.0-rc1" }
server_fn_macro = { path = "./server_fn_macro", version = "0.7.0-rc1" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.0-rc1" }
tachys = { path = "./tachys", version = "0.1.0-rc1" }
[profile.release]
codegen-units = 1
@@ -78,9 +78,3 @@ opt-level = 'z'
[workspace.metadata.cargo-all-features]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
[workspace.lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = [
'cfg(leptos_debuginfo)',
'cfg(erase_components)',
] }

View File

@@ -5,7 +5,6 @@
[![crates.io](https://img.shields.io/crates/v/leptos.svg)](https://crates.io/crates/leptos)
[![docs.rs](https://docs.rs/leptos/badge.svg)](https://docs.rs/leptos)
![Crates.io MSRV](https://img.shields.io/crates/msrv/leptos)
[![Discord](https://img.shields.io/discord/1031524867910148188?color=%237289DA&label=discord)](https://discord.gg/YdRAhS7eQB)
[![Matrix](https://img.shields.io/badge/Matrix-leptos-grey?logo=matrix&labelColor=white&logoColor=black)](https://matrix.to/#/#leptos:matrix.org)
@@ -13,6 +12,8 @@
You can find a list of useful libraries and example projects at [`awesome-leptos`](https://github.com/leptos-rs/awesome-leptos).
# The `main` branch is currently undergoing major changes in preparation for the [0.7](https://github.com/leptos-rs/leptos/milestone/4) release. For a stable version, please use the [v0.6.13 tag](https://github.com/leptos-rs/leptos/tree/v0.6.13)
# Leptos
```rust
@@ -158,7 +159,9 @@ Sure! Obviously the `view` macro is for generating DOM nodes but you can use the
- Use event listeners to update signals
- Create effects to update the UI
The 0.7 update originally set out to create a "generic rendering" approach that would allow us to reuse most of the same view logic to do all of the above. Unfortunately, this has had to be shelved for now due to difficulties encountered by the Rust compiler when building larger-scale applications with the number of generics spread throughout the codebase that this required. It's an approach I'm looking forward to exploring again in the future; feel free to reach out if you're interested in this kind of work.
I've put together a [very simple GTK example](https://github.com/leptos-rs/leptos/blob/main/examples/gtk/src/main.rs) so you can see what I mean.
The new rendering approach being developed for 0.7 supports “universal rendering,” i.e., it can use any rendering library that supports a small set of 6-8 functions. (This is intended as a layer over typical retained-mode, OOP-style GUI toolkits like the DOM, GTK, etc.) That future rendering work will allow creating native UI in a way that is much more similar to the declarative approach used by the web framework.
### How is this different from Yew?
@@ -168,14 +171,14 @@ Yew is the most-used library for Rust web UI development, but there are several
- **Performance:** This has huge performance implications: Leptos is simply much faster at both creating and updating the UI than Yew is.
- **Server integration:** Yew was created in an era in which browser-rendered single-page apps (SPAs) were the dominant paradigm. While Leptos supports client-side rendering, it also focuses on integrating with the server side of your application via server functions and multiple modes of serving HTML, including out-of-order streaming.
### How is this different from Dioxus?
- ### How is this different from Dioxus?
Like Leptos, Dioxus is a framework for building UIs using web technologies. However, there are significant differences in approach and features.
- **VDOM vs. fine-grained:** While Dioxus has a performant virtual DOM (VDOM), it still uses coarse-grained/component-scoped reactivity: changing a stateful value reruns the component function and diffs the old UI against the new one. Leptos components use a different mental model, creating (and returning) actual DOM nodes and setting up a reactive system to update those DOM nodes.
- **Web vs. desktop priorities:** Dioxus uses Leptos server functions in its fullstack mode, but does not have the same `<Suspense>`-based support for things like streaming HTML rendering, or share the same focus on holistic web performance. Leptos tends to prioritize holistic web performance (streaming HTML rendering, smaller WASM binary sizes, etc.), whereas Dioxus has an unparalleled experience when building desktop apps, because your application logic runs as a native Rust binary.
### How is this different from Sycamore?
- ### How is this different from Sycamore?
Sycamore and Leptos are both heavily influenced by SolidJS. At this point, Leptos has a larger community and ecosystem and is more actively developed. Other differences:

View File

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

View File

@@ -17,6 +17,11 @@ use std::{
/* Wrapper Types */
/// This is a result type into which any error can be converted.
///
/// Results are stored as [`Error`].
pub type Result<T, E = Error> = core::result::Result<T, E>;
/// A generic wrapper for any error.
#[derive(Debug, Clone)]
#[repr(transparent)]

View File

@@ -1,6 +1,6 @@
[package]
name = "any_spawner"
version = "0.2.1"
version = "0.1.1"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
@@ -11,13 +11,13 @@ edition.workspace = true
[dependencies]
async-executor = { version = "1.13.1", optional = true }
futures = "0.3.31"
glib = { version = "0.20.6", optional = true }
glib = { version = "0.20.5", optional = true }
thiserror = "2.0"
tokio = { version = "1.41", optional = true, default-features = false, features = [
"rt",
] }
tracing = { version = "0.1.41", optional = true }
wasm-bindgen-futures = { version = "0.4.47", optional = true }
tracing = { version = "0.1.40", optional = true }
wasm-bindgen-futures = { version = "0.4.45", optional = true }
[features]
async-executor = ["dep:async-executor"]

View File

@@ -1,6 +1,6 @@
[package]
name = "either_of"
version = "0.1.4"
version = "0.1.0"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
@@ -10,9 +10,4 @@ rust-version.workspace = true
edition.workspace = true
[dependencies]
pin-project-lite = "0.2.16"
paste = "1.0.15"
[features]
default = ["no_std"]
no_std = []
pin-project-lite = "0.2.15"

View File

@@ -1,758 +1,140 @@
#![cfg_attr(feature = "no_std", no_std)]
#![no_std]
#![forbid(unsafe_code)]
//! Utilities for working with enumerated types that contain one of `2..n` other types.
use core::{
cmp::Ordering,
fmt::Display,
future::Future,
iter::{Product, Sum},
pin::Pin,
task::{Context, Poll},
};
use paste::paste;
use pin_project_lite::pin_project;
#[cfg(not(feature = "no_std"))]
use std::error::Error; // TODO: replace with core::error::Error once MSRV is >= 1.81.0
#[derive(Debug, Clone, Copy)]
pub enum Either<A, B> {
Left(A),
Right(B),
}
impl<Item, A, B> Iterator for Either<A, B>
where
A: Iterator<Item = Item>,
B: Iterator<Item = Item>,
{
type Item = Item;
fn next(&mut self) -> Option<Self::Item> {
match self {
Either::Left(i) => i.next(),
Either::Right(i) => i.next(),
}
}
}
pin_project! {
#[project = EitherFutureProj]
pub enum EitherFuture<A, B> {
Left { #[pin] inner: A },
Right { #[pin] inner: B },
}
}
impl<A, B> Future for EitherFuture<A, B>
where
A: Future,
B: Future,
{
type Output = Either<A::Output, B::Output>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
match this {
EitherFutureProj::Left { inner } => match inner.poll(cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(inner) => Poll::Ready(Either::Left(inner)),
},
EitherFutureProj::Right { inner } => match inner.poll(cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(inner) => Poll::Ready(Either::Right(inner)),
},
}
}
}
macro_rules! tuples {
($name:ident + $fut_name:ident + $fut_proj:ident {
$($ty:ident => ($($rest_variant:ident),*) + <$($mapped_ty:ident),+>),+$(,)?
}) => {
tuples!($name + $fut_name + $fut_proj {
$($ty($ty) => ($($rest_variant),*) + <$($mapped_ty),+>),+
});
};
($name:ident + $fut_name:ident + $fut_proj:ident {
$($variant:ident($ty:ident) => ($($rest_variant:ident),*) + <$($mapped_ty:ident),+>),+$(,)?
}) => {
($name:ident + $fut_name:ident + $fut_proj:ident => $($ty:ident),*) => {
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
pub enum $name<$($ty),+> {
$($variant ($ty),)+
pub enum $name<$($ty,)*> {
$($ty ($ty),)*
}
impl<$($ty),+> $name<$($ty),+> {
paste! {
#[allow(clippy::too_many_arguments)]
pub fn map<$([<F $ty>]),+, $([<$ty 1>]),+>(self, $([<$variant:lower>]: [<F $ty>]),+) -> $name<$([<$ty 1>]),+>
where
$([<F $ty>]: FnOnce($ty) -> [<$ty 1>],)+
{
match self {
$($name::$variant(inner) => $name::$variant([<$variant:lower>](inner)),)+
}
}
$(
pub fn [<map_ $variant:lower>]<Fun, [<$ty 1>]>(self, f: Fun) -> $name<$($mapped_ty),+>
where
Fun: FnOnce($ty) -> [<$ty 1>],
{
match self {
$name::$variant(inner) => $name::$variant(f(inner)),
$($name::$rest_variant(inner) => $name::$rest_variant(inner),)*
}
}
pub fn [<inspect_ $variant:lower>]<Fun, [<$ty 1>]>(self, f: Fun) -> Self
where
Fun: FnOnce(&$ty),
{
if let $name::$variant(inner) = &self {
f(inner);
}
self
}
pub fn [<is_ $variant:lower>](&self) -> bool {
matches!(self, $name::$variant(_))
}
pub fn [<as_ $variant:lower>](&self) -> Option<&$ty> {
match self {
$name::$variant(inner) => Some(inner),
_ => None,
}
}
pub fn [<as_ $variant:lower _mut>](&mut self) -> Option<&mut $ty> {
match self {
$name::$variant(inner) => Some(inner),
_ => None,
}
}
pub fn [<unwrap_ $variant:lower>](self) -> $ty {
match self {
$name::$variant(inner) => inner,
_ => panic!(concat!(
"called `unwrap_", stringify!([<$variant:lower>]), "()` on a non-`", stringify!($variant), "` variant of `", stringify!($name), "`"
)),
}
}
pub fn [<into_ $variant:lower>](self) -> Result<$ty, Self> {
match self {
$name::$variant(inner) => Ok(inner),
_ => Err(self),
}
}
)+
}
}
impl<$($ty),+> Display for $name<$($ty),+>
impl<$($ty,)*> Display for $name<$($ty,)*>
where
$($ty: Display,)+
$($ty: Display,)*
{
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
$($name::$variant(this) => this.fmt(f),)+
$($name::$ty(this) => this.fmt(f),)*
}
}
}
#[cfg(not(feature = "no_std"))]
impl<$($ty),+> Error for $name<$($ty),+>
impl<Item, $($ty,)*> Iterator for $name<$($ty,)*>
where
$($ty: Error,)+
{
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
$($name::$variant(this) => this.source(),)+
}
}
}
impl<Item, $($ty),+> Iterator for $name<$($ty),+>
where
$($ty: Iterator<Item = Item>,)+
$($ty: Iterator<Item = Item>,)*
{
type Item = Item;
fn next(&mut self) -> Option<Self::Item> {
match self {
$($name::$variant(i) => i.next(),)+
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
match self {
$($name::$variant(i) => i.size_hint(),)+
}
}
fn count(self) -> usize
where
Self: Sized,
{
match self {
$($name::$variant(i) => i.count(),)+
}
}
fn last(self) -> Option<Self::Item>
where
Self: Sized,
{
match self {
$($name::$variant(i) => i.last(),)+
}
}
fn nth(&mut self, n: usize) -> Option<Self::Item> {
match self {
$($name::$variant(i) => i.nth(n),)+
}
}
fn for_each<Fun>(self, f: Fun)
where
Self: Sized,
Fun: FnMut(Self::Item),
{
match self {
$($name::$variant(i) => i.for_each(f),)+
}
}
fn collect<Col: FromIterator<Self::Item>>(self) -> Col
where
Self: Sized,
{
match self {
$($name::$variant(i) => i.collect(),)+
}
}
fn partition<Col, Fun>(self, f: Fun) -> (Col, Col)
where
Self: Sized,
Col: Default + Extend<Self::Item>,
Fun: FnMut(&Self::Item) -> bool,
{
match self {
$($name::$variant(i) => i.partition(f),)+
}
}
fn fold<Acc, Fun>(self, init: Acc, f: Fun) -> Acc
where
Self: Sized,
Fun: FnMut(Acc, Self::Item) -> Acc,
{
match self {
$($name::$variant(i) => i.fold(init, f),)+
}
}
fn reduce<Fun>(self, f: Fun) -> Option<Self::Item>
where
Self: Sized,
Fun: FnMut(Self::Item, Self::Item) -> Self::Item,
{
match self {
$($name::$variant(i) => i.reduce(f),)+
}
}
fn all<Fun>(&mut self, f: Fun) -> bool
where
Self: Sized,
Fun: FnMut(Self::Item) -> bool,
{
match self {
$($name::$variant(i) => i.all(f),)+
}
}
fn any<Fun>(&mut self, f: Fun) -> bool
where
Self: Sized,
Fun: FnMut(Self::Item) -> bool,
{
match self {
$($name::$variant(i) => i.any(f),)+
}
}
fn find<Pre>(&mut self, predicate: Pre) -> Option<Self::Item>
where
Self: Sized,
Pre: FnMut(&Self::Item) -> bool,
{
match self {
$($name::$variant(i) => i.find(predicate),)+
}
}
fn find_map<Out, Fun>(&mut self, f: Fun) -> Option<Out>
where
Self: Sized,
Fun: FnMut(Self::Item) -> Option<Out>,
{
match self {
$($name::$variant(i) => i.find_map(f),)+
}
}
fn position<Pre>(&mut self, predicate: Pre) -> Option<usize>
where
Self: Sized,
Pre: FnMut(Self::Item) -> bool,
{
match self {
$($name::$variant(i) => i.position(predicate),)+
}
}
fn max(self) -> Option<Self::Item>
where
Self: Sized,
Self::Item: Ord,
{
match self {
$($name::$variant(i) => i.max(),)+
}
}
fn min(self) -> Option<Self::Item>
where
Self: Sized,
Self::Item: Ord,
{
match self {
$($name::$variant(i) => i.min(),)+
}
}
fn max_by_key<Key: Ord, Fun>(self, f: Fun) -> Option<Self::Item>
where
Self: Sized,
Fun: FnMut(&Self::Item) -> Key,
{
match self {
$($name::$variant(i) => i.max_by_key(f),)+
}
}
fn max_by<Cmp>(self, compare: Cmp) -> Option<Self::Item>
where
Self: Sized,
Cmp: FnMut(&Self::Item, &Self::Item) -> Ordering,
{
match self {
$($name::$variant(i) => i.max_by(compare),)+
}
}
fn min_by_key<Key: Ord, Fun>(self, f: Fun) -> Option<Self::Item>
where
Self: Sized,
Fun: FnMut(&Self::Item) -> Key,
{
match self {
$($name::$variant(i) => i.min_by_key(f),)+
}
}
fn min_by<Cmp>(self, compare: Cmp) -> Option<Self::Item>
where
Self: Sized,
Cmp: FnMut(&Self::Item, &Self::Item) -> Ordering,
{
match self {
$($name::$variant(i) => i.min_by(compare),)+
}
}
fn sum<Out>(self) -> Out
where
Self: Sized,
Out: Sum<Self::Item>,
{
match self {
$($name::$variant(i) => i.sum(),)+
}
}
fn product<Out>(self) -> Out
where
Self: Sized,
Out: Product<Self::Item>,
{
match self {
$($name::$variant(i) => i.product(),)+
}
}
fn cmp<Other>(self, other: Other) -> Ordering
where
Other: IntoIterator<Item = Self::Item>,
Self::Item: Ord,
Self: Sized,
{
match self {
$($name::$variant(i) => i.cmp(other),)+
}
}
fn partial_cmp<Other>(self, other: Other) -> Option<Ordering>
where
Other: IntoIterator,
Self::Item: PartialOrd<Other::Item>,
Self: Sized,
{
match self {
$($name::$variant(i) => i.partial_cmp(other),)+
}
}
// TODO: uncomment once MSRV is >= 1.82.0
// fn is_sorted(self) -> bool
// where
// Self: Sized,
// Self::Item: PartialOrd,
// {
// match self {
// $($name::$variant(i) => i.is_sorted(),)+
// }
// }
//
// fn is_sorted_by<Cmp>(self, compare: Cmp) -> bool
// where
// Self: Sized,
// Cmp: FnMut(&Self::Item, &Self::Item) -> bool,
// {
// match self {
// $($name::$variant(i) => i.is_sorted_by(compare),)+
// }
// }
//
// fn is_sorted_by_key<Fun, Key>(self, f: Fun) -> bool
// where
// Self: Sized,
// Fun: FnMut(Self::Item) -> Key,
// Key: PartialOrd,
// {
// match self {
// $($name::$variant(i) => i.is_sorted_by_key(f),)+
// }
// }
}
impl<Item, $($ty),+> ExactSizeIterator for $name<$($ty),+>
where
$($ty: ExactSizeIterator<Item = Item>,)+
{
fn len(&self) -> usize {
match self {
$($name::$variant(i) => i.len(),)+
}
}
}
impl<Item, $($ty),+> DoubleEndedIterator for $name<$($ty),+>
where
$($ty: DoubleEndedIterator<Item = Item>,)+
{
fn next_back(&mut self) -> Option<Self::Item> {
match self {
$($name::$variant(i) => i.next_back(),)+
}
}
fn nth_back(&mut self, n: usize) -> Option<Self::Item> {
match self {
$($name::$variant(i) => i.nth_back(n),)+
}
}
fn rfind<Pre>(&mut self, predicate: Pre) -> Option<Self::Item>
where
Pre: FnMut(&Self::Item) -> bool,
{
match self {
$($name::$variant(i) => i.rfind(predicate),)+
$($name::$ty(i) => i.next(),)*
}
}
}
pin_project! {
#[project = $fut_proj]
pub enum $fut_name<$($ty),+> {
$($variant { #[pin] inner: $ty },)+
pub enum $fut_name<$($ty,)*> {
$($ty { #[pin] inner: $ty },)*
}
}
impl<$($ty),+> Future for $fut_name<$($ty),+>
impl<$($ty,)*> Future for $fut_name<$($ty,)*>
where
$($ty: Future,)+
$($ty: Future,)*
{
type Output = $name<$($ty::Output),+>;
type Output = $name<$($ty::Output,)*>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
match this {
$($fut_proj::$variant { inner } => match inner.poll(cx) {
$($fut_proj::$ty { inner } => match inner.poll(cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(inner) => Poll::Ready($name::$variant(inner)),
},)+
Poll::Ready(inner) => Poll::Ready($name::$ty(inner)),
},)*
}
}
}
}
}
tuples!(Either + EitherFuture + EitherFutureProj {
Left(A) => (Right) + <A1, B>,
Right(B) => (Left) + <A, B1>,
});
tuples!(EitherOf3 + EitherOf3Future + EitherOf3FutureProj => A, B, C);
tuples!(EitherOf4 + EitherOf4Future + EitherOf4FutureProj => A, B, C, D);
tuples!(EitherOf5 + EitherOf5Future + EitherOf5FutureProj => A, B, C, D, E);
tuples!(EitherOf6 + EitherOf6Future + EitherOf6FutureProj => A, B, C, D, E, F);
tuples!(EitherOf7 + EitherOf7Future + EitherOf7FutureProj => A, B, C, D, E, F, G);
tuples!(EitherOf8 + EitherOf8Future + EitherOf8FutureProj => A, B, C, D, E, F, G, H);
tuples!(EitherOf9 + EitherOf9Future + EitherOf9FutureProj => A, B, C, D, E, F, G, H, I);
tuples!(EitherOf10 + EitherOf10Future + EitherOf10FutureProj => A, B, C, D, E, F, G, H, I, J);
tuples!(EitherOf11 + EitherOf11Future + EitherOf11FutureProj => A, B, C, D, E, F, G, H, I, J, K);
tuples!(EitherOf12 + EitherOf12Future + EitherOf12FutureProj => A, B, C, D, E, F, G, H, I, J, K, L);
tuples!(EitherOf13 + EitherOf13Future + EitherOf13FutureProj => A, B, C, D, E, F, G, H, I, J, K, L, M);
tuples!(EitherOf14 + EitherOf14Future + EitherOf14FutureProj => A, B, C, D, E, F, G, H, I, J, K, L, M, N);
tuples!(EitherOf15 + EitherOf15Future + EitherOf15FutureProj => A, B, C, D, E, F, G, H, I, J, K, L, M, N, O);
tuples!(EitherOf16 + EitherOf16Future + EitherOf16FutureProj => A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P);
impl<A, B> Either<A, B> {
pub fn swap(self) -> Either<B, A> {
match self {
Either::Left(a) => Either::Right(a),
Either::Right(b) => Either::Left(b),
}
}
}
impl<A, B> From<Result<A, B>> for Either<A, B> {
fn from(value: Result<A, B>) -> Self {
match value {
Ok(left) => Either::Left(left),
Err(right) => Either::Right(right),
}
}
}
pub trait EitherOr {
type Left;
type Right;
fn either_or<FA, A, FB, B>(self, a: FA, b: FB) -> Either<A, B>
where
FA: FnOnce(Self::Left) -> A,
FB: FnOnce(Self::Right) -> B;
}
impl EitherOr for bool {
type Left = ();
type Right = ();
fn either_or<FA, A, FB, B>(self, a: FA, b: FB) -> Either<A, B>
where
FA: FnOnce(Self::Left) -> A,
FB: FnOnce(Self::Right) -> B,
{
if self {
Either::Left(a(()))
} else {
Either::Right(b(()))
}
}
}
impl<T> EitherOr for Option<T> {
type Left = T;
type Right = ();
fn either_or<FA, A, FB, B>(self, a: FA, b: FB) -> Either<A, B>
where
FA: FnOnce(Self::Left) -> A,
FB: FnOnce(Self::Right) -> B,
{
match self {
Some(t) => Either::Left(a(t)),
None => Either::Right(b(())),
}
}
}
impl<T, E> EitherOr for Result<T, E> {
type Left = T;
type Right = E;
fn either_or<FA, A, FB, B>(self, a: FA, b: FB) -> Either<A, B>
where
FA: FnOnce(Self::Left) -> A,
FB: FnOnce(Self::Right) -> B,
{
match self {
Ok(t) => Either::Left(a(t)),
Err(err) => Either::Right(b(err)),
}
}
}
impl<A, B> EitherOr for Either<A, B> {
type Left = A;
type Right = B;
#[inline]
fn either_or<FA, A1, FB, B1>(self, a: FA, b: FB) -> Either<A1, B1>
where
FA: FnOnce(<Self as EitherOr>::Left) -> A1,
FB: FnOnce(<Self as EitherOr>::Right) -> B1,
{
self.map(a, b)
}
}
#[test]
fn test_either_or() {
let right = false.either_or(|_| 'a', |_| 12);
assert!(matches!(right, Either::Right(12)));
let left = true.either_or(|_| 'a', |_| 12);
assert!(matches!(left, Either::Left('a')));
let left = Some(12).either_or(|a| a, |_| 'a');
assert!(matches!(left, Either::Left(12)));
let right = None.either_or(|a: i32| a, |_| 'a');
assert!(matches!(right, Either::Right('a')));
let result: Result<_, ()> = Ok(1.2f32);
let left = result.either_or(|a| a * 2f32, |b| b);
assert!(matches!(left, Either::Left(2.4f32)));
let result: Result<i32, _> = Err("12");
let right = result.either_or(|a| a, |b| b.chars().next());
assert!(matches!(right, Either::Right(Some('1'))));
let either = Either::<i32, char>::Left(12);
let left = either.either_or(|a| a, |b| b);
assert!(matches!(left, Either::Left(12)));
let either = Either::<i32, char>::Right('a');
let right = either.either_or(|a| a, |b| b);
assert!(matches!(right, Either::Right('a')));
}
tuples!(EitherOf3 + EitherOf3Future + EitherOf3FutureProj {
A => (B, C) + <A1, B, C>,
B => (A, C) + <A, B1, C>,
C => (A, B) + <A, B, C1>,
});
tuples!(EitherOf4 + EitherOf4Future + EitherOf4FutureProj {
A => (B, C, D) + <A1, B, C, D>,
B => (A, C, D) + <A, B1, C, D>,
C => (A, B, D) + <A, B, C1, D>,
D => (A, B, C) + <A, B, C, D1>,
});
tuples!(EitherOf5 + EitherOf5Future + EitherOf5FutureProj {
A => (B, C, D, E) + <A1, B, C, D, E>,
B => (A, C, D, E) + <A, B1, C, D, E>,
C => (A, B, D, E) + <A, B, C1, D, E>,
D => (A, B, C, E) + <A, B, C, D1, E>,
E => (A, B, C, D) + <A, B, C, D, E1>,
});
tuples!(EitherOf6 + EitherOf6Future + EitherOf6FutureProj {
A => (B, C, D, E, F) + <A1, B, C, D, E, F>,
B => (A, C, D, E, F) + <A, B1, C, D, E, F>,
C => (A, B, D, E, F) + <A, B, C1, D, E, F>,
D => (A, B, C, E, F) + <A, B, C, D1, E, F>,
E => (A, B, C, D, F) + <A, B, C, D, E1, F>,
F => (A, B, C, D, E) + <A, B, C, D, E, F1>,
});
tuples!(EitherOf7 + EitherOf7Future + EitherOf7FutureProj {
A => (B, C, D, E, F, G) + <A1, B, C, D, E, F, G>,
B => (A, C, D, E, F, G) + <A, B1, C, D, E, F, G>,
C => (A, B, D, E, F, G) + <A, B, C1, D, E, F, G>,
D => (A, B, C, E, F, G) + <A, B, C, D1, E, F, G>,
E => (A, B, C, D, F, G) + <A, B, C, D, E1, F, G>,
F => (A, B, C, D, E, G) + <A, B, C, D, E, F1, G>,
G => (A, B, C, D, E, F) + <A, B, C, D, E, F, G1>,
});
tuples!(EitherOf8 + EitherOf8Future + EitherOf8FutureProj {
A => (B, C, D, E, F, G, H) + <A1, B, C, D, E, F, G, H>,
B => (A, C, D, E, F, G, H) + <A, B1, C, D, E, F, G, H>,
C => (A, B, D, E, F, G, H) + <A, B, C1, D, E, F, G, H>,
D => (A, B, C, E, F, G, H) + <A, B, C, D1, E, F, G, H>,
E => (A, B, C, D, F, G, H) + <A, B, C, D, E1, F, G, H>,
F => (A, B, C, D, E, G, H) + <A, B, C, D, E, F1, G, H>,
G => (A, B, C, D, E, F, H) + <A, B, C, D, E, F, G1, H>,
H => (A, B, C, D, E, F, G) + <A, B, C, D, E, F, G, H1>,
});
tuples!(EitherOf9 + EitherOf9Future + EitherOf9FutureProj {
A => (B, C, D, E, F, G, H, I) + <A1, B, C, D, E, F, G, H, I>,
B => (A, C, D, E, F, G, H, I) + <A, B1, C, D, E, F, G, H, I>,
C => (A, B, D, E, F, G, H, I) + <A, B, C1, D, E, F, G, H, I>,
D => (A, B, C, E, F, G, H, I) + <A, B, C, D1, E, F, G, H, I>,
E => (A, B, C, D, F, G, H, I) + <A, B, C, D, E1, F, G, H, I>,
F => (A, B, C, D, E, G, H, I) + <A, B, C, D, E, F1, G, H, I>,
G => (A, B, C, D, E, F, H, I) + <A, B, C, D, E, F, G1, H, I>,
H => (A, B, C, D, E, F, G, I) + <A, B, C, D, E, F, G, H1, I>,
I => (A, B, C, D, E, F, G, H) + <A, B, C, D, E, F, G, H, I1>,
});
tuples!(EitherOf10 + EitherOf10Future + EitherOf10FutureProj {
A => (B, C, D, E, F, G, H, I, J) + <A1, B, C, D, E, F, G, H, I, J>,
B => (A, C, D, E, F, G, H, I, J) + <A, B1, C, D, E, F, G, H, I, J>,
C => (A, B, D, E, F, G, H, I, J) + <A, B, C1, D, E, F, G, H, I, J>,
D => (A, B, C, E, F, G, H, I, J) + <A, B, C, D1, E, F, G, H, I, J>,
E => (A, B, C, D, F, G, H, I, J) + <A, B, C, D, E1, F, G, H, I, J>,
F => (A, B, C, D, E, G, H, I, J) + <A, B, C, D, E, F1, G, H, I, J>,
G => (A, B, C, D, E, F, H, I, J) + <A, B, C, D, E, F, G1, H, I, J>,
H => (A, B, C, D, E, F, G, I, J) + <A, B, C, D, E, F, G, H1, I, J>,
I => (A, B, C, D, E, F, G, H, J) + <A, B, C, D, E, F, G, H, I1, J>,
J => (A, B, C, D, E, F, G, H, I) + <A, B, C, D, E, F, G, H, I, J1>,
});
tuples!(EitherOf11 + EitherOf11Future + EitherOf11FutureProj {
A => (B, C, D, E, F, G, H, I, J, K) + <A1, B, C, D, E, F, G, H, I, J, K>,
B => (A, C, D, E, F, G, H, I, J, K) + <A, B1, C, D, E, F, G, H, I, J, K>,
C => (A, B, D, E, F, G, H, I, J, K) + <A, B, C1, D, E, F, G, H, I, J, K>,
D => (A, B, C, E, F, G, H, I, J, K) + <A, B, C, D1, E, F, G, H, I, J, K>,
E => (A, B, C, D, F, G, H, I, J, K) + <A, B, C, D, E1, F, G, H, I, J, K>,
F => (A, B, C, D, E, G, H, I, J, K) + <A, B, C, D, E, F1, G, H, I, J, K>,
G => (A, B, C, D, E, F, H, I, J, K) + <A, B, C, D, E, F, G1, H, I, J, K>,
H => (A, B, C, D, E, F, G, I, J, K) + <A, B, C, D, E, F, G, H1, I, J, K>,
I => (A, B, C, D, E, F, G, H, J, K) + <A, B, C, D, E, F, G, H, I1, J, K>,
J => (A, B, C, D, E, F, G, H, I, K) + <A, B, C, D, E, F, G, H, I, J1, K>,
K => (A, B, C, D, E, F, G, H, I, J) + <A, B, C, D, E, F, G, H, I, J, K1>,
});
tuples!(EitherOf12 + EitherOf12Future + EitherOf12FutureProj {
A => (B, C, D, E, F, G, H, I, J, K, L) + <A1, B, C, D, E, F, G, H, I, J, K, L>,
B => (A, C, D, E, F, G, H, I, J, K, L) + <A, B1, C, D, E, F, G, H, I, J, K, L>,
C => (A, B, D, E, F, G, H, I, J, K, L) + <A, B, C1, D, E, F, G, H, I, J, K, L>,
D => (A, B, C, E, F, G, H, I, J, K, L) + <A, B, C, D1, E, F, G, H, I, J, K, L>,
E => (A, B, C, D, F, G, H, I, J, K, L) + <A, B, C, D, E1, F, G, H, I, J, K, L>,
F => (A, B, C, D, E, G, H, I, J, K, L) + <A, B, C, D, E, F1, G, H, I, J, K, L>,
G => (A, B, C, D, E, F, H, I, J, K, L) + <A, B, C, D, E, F, G1, H, I, J, K, L>,
H => (A, B, C, D, E, F, G, I, J, K, L) + <A, B, C, D, E, F, G, H1, I, J, K, L>,
I => (A, B, C, D, E, F, G, H, J, K, L) + <A, B, C, D, E, F, G, H, I1, J, K, L>,
J => (A, B, C, D, E, F, G, H, I, K, L) + <A, B, C, D, E, F, G, H, I, J1, K, L>,
K => (A, B, C, D, E, F, G, H, I, J, L) + <A, B, C, D, E, F, G, H, I, J, K1, L>,
L => (A, B, C, D, E, F, G, H, I, J, K) + <A, B, C, D, E, F, G, H, I, J, K, L1>,
});
tuples!(EitherOf13 + EitherOf13Future + EitherOf13FutureProj {
A => (B, C, D, E, F, G, H, I, J, K, L, M) + <A1, B, C, D, E, F, G, H, I, J, K, L, M>,
B => (A, C, D, E, F, G, H, I, J, K, L, M) + <A, B1, C, D, E, F, G, H, I, J, K, L, M>,
C => (A, B, D, E, F, G, H, I, J, K, L, M) + <A, B, C1, D, E, F, G, H, I, J, K, L, M>,
D => (A, B, C, E, F, G, H, I, J, K, L, M) + <A, B, C, D1, E, F, G, H, I, J, K, L, M>,
E => (A, B, C, D, F, G, H, I, J, K, L, M) + <A, B, C, D, E1, F, G, H, I, J, K, L, M>,
F => (A, B, C, D, E, G, H, I, J, K, L, M) + <A, B, C, D, E, F1, G, H, I, J, K, L, M>,
G => (A, B, C, D, E, F, H, I, J, K, L, M) + <A, B, C, D, E, F, G1, H, I, J, K, L, M>,
H => (A, B, C, D, E, F, G, I, J, K, L, M) + <A, B, C, D, E, F, G, H1, I, J, K, L, M>,
I => (A, B, C, D, E, F, G, H, J, K, L, M) + <A, B, C, D, E, F, G, H, I1, J, K, L, M>,
J => (A, B, C, D, E, F, G, H, I, K, L, M) + <A, B, C, D, E, F, G, H, I, J1, K, L, M>,
K => (A, B, C, D, E, F, G, H, I, J, L, M) + <A, B, C, D, E, F, G, H, I, J, K1, L, M>,
L => (A, B, C, D, E, F, G, H, I, J, K, M) + <A, B, C, D, E, F, G, H, I, J, K, L1, M>,
M => (A, B, C, D, E, F, G, H, I, J, K, L) + <A, B, C, D, E, F, G, H, I, J, K, L, M1>,
});
tuples!(EitherOf14 + EitherOf14Future + EitherOf14FutureProj {
A => (B, C, D, E, F, G, H, I, J, K, L, M, N) + <A1, B, C, D, E, F, G, H, I, J, K, L, M, N>,
B => (A, C, D, E, F, G, H, I, J, K, L, M, N) + <A, B1, C, D, E, F, G, H, I, J, K, L, M, N>,
C => (A, B, D, E, F, G, H, I, J, K, L, M, N) + <A, B, C1, D, E, F, G, H, I, J, K, L, M, N>,
D => (A, B, C, E, F, G, H, I, J, K, L, M, N) + <A, B, C, D1, E, F, G, H, I, J, K, L, M, N>,
E => (A, B, C, D, F, G, H, I, J, K, L, M, N) + <A, B, C, D, E1, F, G, H, I, J, K, L, M, N>,
F => (A, B, C, D, E, G, H, I, J, K, L, M, N) + <A, B, C, D, E, F1, G, H, I, J, K, L, M, N>,
G => (A, B, C, D, E, F, H, I, J, K, L, M, N) + <A, B, C, D, E, F, G1, H, I, J, K, L, M, N>,
H => (A, B, C, D, E, F, G, I, J, K, L, M, N) + <A, B, C, D, E, F, G, H1, I, J, K, L, M, N>,
I => (A, B, C, D, E, F, G, H, J, K, L, M, N) + <A, B, C, D, E, F, G, H, I1, J, K, L, M, N>,
J => (A, B, C, D, E, F, G, H, I, K, L, M, N) + <A, B, C, D, E, F, G, H, I, J1, K, L, M, N>,
K => (A, B, C, D, E, F, G, H, I, J, L, M, N) + <A, B, C, D, E, F, G, H, I, J, K1, L, M, N>,
L => (A, B, C, D, E, F, G, H, I, J, K, M, N) + <A, B, C, D, E, F, G, H, I, J, K, L1, M, N>,
M => (A, B, C, D, E, F, G, H, I, J, K, L, N) + <A, B, C, D, E, F, G, H, I, J, K, L, M1, N>,
N => (A, B, C, D, E, F, G, H, I, J, K, L, M) + <A, B, C, D, E, F, G, H, I, J, K, L, M, N1>,
});
tuples!(EitherOf15 + EitherOf15Future + EitherOf15FutureProj {
A => (B, C, D, E, F, G, H, I, J, K, L, M, N, O) + <A1, B, C, D, E, F, G, H, I, J, K, L, M, N, O>,
B => (A, C, D, E, F, G, H, I, J, K, L, M, N, O) + <A, B1, C, D, E, F, G, H, I, J, K, L, M, N, O>,
C => (A, B, D, E, F, G, H, I, J, K, L, M, N, O) + <A, B, C1, D, E, F, G, H, I, J, K, L, M, N, O>,
D => (A, B, C, E, F, G, H, I, J, K, L, M, N, O) + <A, B, C, D1, E, F, G, H, I, J, K, L, M, N, O>,
E => (A, B, C, D, F, G, H, I, J, K, L, M, N, O) + <A, B, C, D, E1, F, G, H, I, J, K, L, M, N, O>,
F => (A, B, C, D, E, G, H, I, J, K, L, M, N, O) + <A, B, C, D, E, F1, G, H, I, J, K, L, M, N, O>,
G => (A, B, C, D, E, F, H, I, J, K, L, M, N, O) + <A, B, C, D, E, F, G1, H, I, J, K, L, M, N, O>,
H => (A, B, C, D, E, F, G, I, J, K, L, M, N, O) + <A, B, C, D, E, F, G, H1, I, J, K, L, M, N, O>,
I => (A, B, C, D, E, F, G, H, J, K, L, M, N, O) + <A, B, C, D, E, F, G, H, I1, J, K, L, M, N, O>,
J => (A, B, C, D, E, F, G, H, I, K, L, M, N, O) + <A, B, C, D, E, F, G, H, I, J1, K, L, M, N, O>,
K => (A, B, C, D, E, F, G, H, I, J, L, M, N, O) + <A, B, C, D, E, F, G, H, I, J, K1, L, M, N, O>,
L => (A, B, C, D, E, F, G, H, I, J, K, M, N, O) + <A, B, C, D, E, F, G, H, I, J, K, L1, M, N, O>,
M => (A, B, C, D, E, F, G, H, I, J, K, L, N, O) + <A, B, C, D, E, F, G, H, I, J, K, L, M1, N, O>,
N => (A, B, C, D, E, F, G, H, I, J, K, L, M, O) + <A, B, C, D, E, F, G, H, I, J, K, L, M, N1, O>,
O => (A, B, C, D, E, F, G, H, I, J, K, L, M, N) + <A, B, C, D, E, F, G, H, I, J, K, L, M, N, O1>,
});
tuples!(EitherOf16 + EitherOf16Future + EitherOf16FutureProj {
A => (B, C, D, E, F, G, H, I, J, K, L, M, N, O, P) + <A1, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P>,
B => (A, C, D, E, F, G, H, I, J, K, L, M, N, O, P) + <A, B1, C, D, E, F, G, H, I, J, K, L, M, N, O, P>,
C => (A, B, D, E, F, G, H, I, J, K, L, M, N, O, P) + <A, B, C1, D, E, F, G, H, I, J, K, L, M, N, O, P>,
D => (A, B, C, E, F, G, H, I, J, K, L, M, N, O, P) + <A, B, C, D1, E, F, G, H, I, J, K, L, M, N, O, P>,
E => (A, B, C, D, F, G, H, I, J, K, L, M, N, O, P) + <A, B, C, D, E1, F, G, H, I, J, K, L, M, N, O, P>,
F => (A, B, C, D, E, G, H, I, J, K, L, M, N, O, P) + <A, B, C, D, E, F1, G, H, I, J, K, L, M, N, O, P>,
G => (A, B, C, D, E, F, H, I, J, K, L, M, N, O, P) + <A, B, C, D, E, F, G1, H, I, J, K, L, M, N, O, P>,
H => (A, B, C, D, E, F, G, I, J, K, L, M, N, O, P) + <A, B, C, D, E, F, G, H1, I, J, K, L, M, N, O, P>,
I => (A, B, C, D, E, F, G, H, J, K, L, M, N, O, P) + <A, B, C, D, E, F, G, H, I1, J, K, L, M, N, O, P>,
J => (A, B, C, D, E, F, G, H, I, K, L, M, N, O, P) + <A, B, C, D, E, F, G, H, I, J1, K, L, M, N, O, P>,
K => (A, B, C, D, E, F, G, H, I, J, L, M, N, O, P) + <A, B, C, D, E, F, G, H, I, J, K1, L, M, N, O, P>,
L => (A, B, C, D, E, F, G, H, I, J, K, M, N, O, P) + <A, B, C, D, E, F, G, H, I, J, K, L1, M, N, O, P>,
M => (A, B, C, D, E, F, G, H, I, J, K, L, N, O, P) + <A, B, C, D, E, F, G, H, I, J, K, L, M1, N, O, P>,
N => (A, B, C, D, E, F, G, H, I, J, K, L, M, O, P) + <A, B, C, D, E, F, G, H, I, J, K, L, M, N1, O, P>,
O => (A, B, C, D, E, F, G, H, I, J, K, L, M, N, P) + <A, B, C, D, E, F, G, H, I, J, K, L, M, N, O1, P>,
P => (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O) + <A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P1>,
});
/// Matches over the first expression and returns an either ([`Either`], [`EitherOf3`], ... [`EitherOf8`])
/// Matches over the first expression and returns an either ([`Either`], [`EitherOf3`], ... [`EitherOf6`])
/// composed of the values returned by the match arms.
///
/// The pattern syntax is exactly the same as found in a match arm.
@@ -815,93 +197,40 @@ macro_rules! either {
$e_pattern => $crate::EitherOf6::E($e_expression),
$f_pattern => $crate::EitherOf6::F($f_expression),
}
};
($match:expr, $a_pattern:pat => $a_expression:expr, $b_pattern:pat => $b_expression:expr, $c_pattern:pat => $c_expression:expr, $d_pattern:pat => $d_expression:expr, $e_pattern:pat => $e_expression:expr, $f_pattern:pat => $f_expression:expr, $g_pattern:pat => $g_expression:expr,) => {
match $match {
$a_pattern => $crate::EitherOf7::A($a_expression),
$b_pattern => $crate::EitherOf7::B($b_expression),
$c_pattern => $crate::EitherOf7::C($c_expression),
$d_pattern => $crate::EitherOf7::D($d_expression),
$e_pattern => $crate::EitherOf7::E($e_expression),
$f_pattern => $crate::EitherOf7::F($f_expression),
$g_pattern => $crate::EitherOf7::G($g_expression),
}
};
($match:expr, $a_pattern:pat => $a_expression:expr, $b_pattern:pat => $b_expression:expr, $c_pattern:pat => $c_expression:expr, $d_pattern:pat => $d_expression:expr, $e_pattern:pat => $e_expression:expr, $f_pattern:pat => $f_expression:expr, $g_pattern:pat => $g_expression:expr, $h_pattern:pat => $h_expression:expr,) => {
match $match {
$a_pattern => $crate::EitherOf8::A($a_expression),
$b_pattern => $crate::EitherOf8::B($b_expression),
$c_pattern => $crate::EitherOf8::C($c_expression),
$d_pattern => $crate::EitherOf8::D($d_expression),
$e_pattern => $crate::EitherOf8::E($e_expression),
$f_pattern => $crate::EitherOf8::F($f_expression),
$g_pattern => $crate::EitherOf8::G($g_expression),
$h_pattern => $crate::EitherOf8::H($h_expression),
}
}; // if you need more eithers feel free to open a PR ;-)
}
#[cfg(test)]
mod tests {
use super::*;
// compile time test
#[test]
fn either_macro() {
let _: Either<&str, f64> = either!(12,
12 => "12",
_ => 0.0,
);
let _: EitherOf3<&str, f64, i32> = either!(12,
12 => "12",
13 => 0.0,
_ => 12,
);
let _: EitherOf4<&str, f64, char, i32> = either!(12,
12 => "12",
13 => 0.0,
14 => ' ',
_ => 12,
);
let _: EitherOf5<&str, f64, char, f32, i32> = either!(12,
12 => "12",
13 => 0.0,
14 => ' ',
15 => 0.0f32,
_ => 12,
);
let _: EitherOf6<&str, f64, char, f32, u8, i32> = either!(12,
12 => "12",
13 => 0.0,
14 => ' ',
15 => 0.0f32,
16 => 24u8,
_ => 12,
);
let _: EitherOf7<&str, f64, char, f32, u8, i8, i32> = either!(12,
12 => "12",
13 => 0.0,
14 => ' ',
15 => 0.0f32,
16 => 24u8,
17 => 2i8,
_ => 12,
);
let _: EitherOf8<&str, f64, char, f32, u8, i8, u32, i32> = either!(12,
12 => "12",
13 => 0.0,
14 => ' ',
15 => 0.0f32,
16 => 24u8,
17 => 2i8,
18 => 42u32,
_ => 12,
);
}
#[test]
#[should_panic]
fn unwrap_wrong_either() {
Either::<i32, &str>::Left(0).unwrap_right();
}
// compile time test
#[test]
fn either_macro() {
let _: Either<&str, f64> = either!(12,
12 => "12",
_ => 0.0,
);
let _: EitherOf3<&str, f64, i32> = either!(12,
12 => "12",
13 => 0.0,
_ => 12,
);
let _: EitherOf4<&str, f64, char, i32> = either!(12,
12 => "12",
13 => 0.0,
14 => ' ',
_ => 12,
);
let _: EitherOf5<&str, f64, char, f32, i32> = either!(12,
12 => "12",
13 => 0.0,
14 => ' ',
15 => 0.0f32,
_ => 12,
);
let _: EitherOf6<&str, f64, char, f32, u8, i32> = either!(12,
12 => "12",
13 => 0.0,
14 => ' ',
15 => 0.0f32,
16 => 24u8,
_ => 12,
);
}

View File

@@ -7,7 +7,7 @@ edition = "2021"
crate-type = ["cdylib", "rlib"]
[dependencies]
axum = { version = "0.8.1", optional = true }
axum = { version = "0.7.5", optional = true }
console_error_panic_hook = "0.1.7"
console_log = "1.0"
gloo-utils = "0.2.0"
@@ -20,27 +20,18 @@ leptos_axum = { path = "../../integrations/axum", optional = true }
leptos_router = { path = "../../router" }
serde = { version = "1.0", features = ["derive"] }
thiserror = "1.0"
tokio = { version = "1.39", features = [
"rt-multi-thread",
"macros",
"time",
], optional = true }
tokio = { version = "1.39", features = [ "rt-multi-thread", "macros", "time" ], optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
wasm-bindgen = "0.2.92"
web-sys = { version = "0.3.69", features = [
"AddEventListenerOptions",
"Document",
"Element",
"Event",
"EventListener",
"EventTarget",
"Performance",
"Window",
], optional = true }
web-sys = { version = "0.3.69", features = [ "AddEventListenerOptions", "Document", "Element", "Event", "EventListener", "EventTarget", "Performance", "Window" ], optional = true }
[features]
hydrate = ["leptos/hydrate", "dep:js-sys", "dep:web-sys"]
hydrate = [
"leptos/hydrate",
"dep:js-sys",
"dep:web-sys",
]
ssr = [
"dep:axum",
"dep:http-body-util",

View File

@@ -1227,4 +1227,4 @@ begin:"\\b[0-9]{4}(-[0-9][0-9]){0,2}([Tt \\t][0-9][0-9]?(:[0-9][0-9]){2})?(\\.[0
;return c.pop(),c.push(i),r.contains=c,{name:"YAML",case_insensitive:!0,
aliases:["yml"],contains:l}}});const Ke=te;for(const e of Object.keys(Pe)){
const n=e.replace("grmr_","").replace("_","-");Ke.registerLanguage(n,Pe[e])}
export{Ke as defaultMod};
export{Ke as default};

View File

@@ -13,13 +13,13 @@ mod csr {
extern "C" {
type HighlightOptions;
#[wasm_bindgen(catch, js_namespace = defaultMod, js_name = highlight)]
#[wasm_bindgen(catch, js_namespace = default, js_name = highlight)]
fn highlight_lang(
code: String,
options: Object,
) -> Result<Object, JsValue>;
#[wasm_bindgen(js_namespace = defaultMod, js_name = highlightAll)]
#[wasm_bindgen(js_namespace = default, js_name = highlightAll)]
pub fn highlight_all();
}

View File

@@ -19,7 +19,7 @@ async fn clear() {
// note that we start at the initial value of 10
let _dispose = mount_to(
test_wrapper.clone().unchecked_into(),
|| view! { <SimpleCounter initial_value=10 step=1 /> },
|| view! { <SimpleCounter initial_value=10 step=1/> },
);
// now we extract the buttons by iterating over the DOM
@@ -59,9 +59,9 @@ async fn clear() {
// .into_view() here is just a convenient way of specifying "use the regular DOM renderer"
.into_view()
// views are lazy -- they describe a DOM tree but don't create it yet
// calling .build(None) will actually build the DOM elements
.build(None)
// .build(None) returned an ElementState, which is a smart pointer for
// calling .build() will actually build the DOM elements
.build()
// .build() returned an ElementState, which is a smart pointer for
// a DOM element. So we can still just call .outer_html(), which access the outerHTML on
// the actual DOM element
.outer_html()
@@ -87,7 +87,7 @@ async fn inc() {
let _dispose = mount_to(
test_wrapper.clone().unchecked_into(),
|| view! { <SimpleCounter initial_value=0 step=1 /> },
|| view! { <SimpleCounter initial_value=0 step=1/> },
);
// You can do testing with vanilla DOM operations
@@ -150,7 +150,7 @@ async fn inc() {
}
}
.into_view()
.build(None)
.build()
.outer_html()
);
@@ -173,7 +173,7 @@ async fn inc() {
}
}
.into_view()
.build(None)
.build()
.outer_html()
);
}

View File

@@ -13,7 +13,7 @@ leptos_axum = { path = "../../integrations/axum", optional = true }
leptos_meta = { path = "../../meta" }
leptos_router = { path = "../../router" }
serde = { version = "1.0", features = ["derive"] }
axum = { version = "0.8.1", optional = true }
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 = ["full"], optional = true }

View File

@@ -45,7 +45,7 @@ async fn main() {
// build our application with a route
let app = Router::new()
.route("/special/{id}", get(custom_handler))
.route("/special/:id", get(custom_handler))
.leptos_routes(&leptos_options, routes, {
let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone())

View File

@@ -20,7 +20,7 @@ serde = { version = "1.0", features = ["derive"] }
tracing = "0.1.40"
gloo-net = { version = "0.6.0", features = ["http"] }
reqwest = { version = "0.12.5", features = ["json"] }
axum = { version = "0.8.1", optional = true }
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 = ["full"], optional = true }

View File

@@ -12,7 +12,7 @@ lto = true
[dependencies]
console_error_panic_hook = "0.1.7"
leptos = { path = "../../leptos", features = ["islands"] }
leptos = { path = "../../leptos", features = ["experimental-islands"] }
leptos_axum = { path = "../../integrations/axum", optional = true }
leptos_meta = { path = "../../meta" }
leptos_router = { path = "../../router" }
@@ -20,7 +20,7 @@ serde = { version = "1.0", features = ["derive"] }
tracing = "0.1.40"
gloo-net = { version = "0.6.0", features = ["http"] }
reqwest = { version = "0.12.5", features = ["json"] }
axum = { version = "0.8.1", optional = true, features = ["http2"] }
axum = { version = "0.7.5", optional = true, features = ["http2"] }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", features = [
"fs",

View File

@@ -23,7 +23,7 @@ serde = { version = "1.0", features = ["derive"] }
tracing = "0.1.40"
gloo-net = { version = "0.6.0", features = ["http"] }
reqwest = { version = "0.12.5", features = ["json"] }
axum = { version = "0.8.1", default-features = false, optional = true }
axum = { version = "0.7.5", default-features = false, optional = true }
tower = { version = "0.4.13", optional = true }
http = { version = "1.1", optional = true }
web-sys = { version = "0.3.70", features = [

View File

@@ -10,12 +10,15 @@ crate-type = ["cdylib", "rlib"]
console_error_panic_hook = "0.1.7"
futures = "0.3.30"
http = "1.1"
leptos = { path = "../../leptos", features = ["tracing", "islands"] }
leptos = { path = "../../leptos", features = [
"tracing",
"experimental-islands",
] }
server_fn = { path = "../../server_fn", features = ["serde-lite"] }
leptos_axum = { path = "../../integrations/axum", optional = true }
log = "0.4.22"
serde = { version = "1.0", features = ["derive"] }
axum = { version = "0.8.1", optional = true }
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 = ["full"], optional = true }

View File

@@ -10,7 +10,10 @@ crate-type = ["cdylib", "rlib"]
console_error_panic_hook = "0.1.7"
futures = "0.3.30"
http = "1.1"
leptos = { path = "../../leptos", features = ["tracing", "islands"] }
leptos = { path = "../../leptos", features = [
"tracing",
"experimental-islands",
] }
leptos_router = { path = "../../router" }
server_fn = { path = "../../server_fn", features = ["serde-lite"] }
leptos_axum = { path = "../../integrations/axum", features = [
@@ -18,7 +21,7 @@ leptos_axum = { path = "../../integrations/axum", features = [
], optional = true }
log = "0.4.22"
serde = { version = "1.0", features = ["derive"] }
axum = { version = "0.8.1", optional = true }
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 = ["full"], optional = true }

View File

@@ -44,8 +44,8 @@ window.addEventListener("click", async (ev) => {
// TODO parse from the request stream instead?
const doc = parser.parseFromString(htmlString, 'text/html');
// The 'doc' variable now contains the parsed DOM
const transition = async () => {
// The 'doc' variable now contains the parsed DOM
const transition = document.startViewTransition(async () => {
const oldDocWalker = document.createTreeWalker(document);
const newDocWalker = doc.createTreeWalker(doc);
let oldNode = oldDocWalker.currentNode;
@@ -128,13 +128,8 @@ window.addEventListener("click", async (ev) => {
}
} }
}
};
// Not all browsers support startViewTransition; see https://caniuse.com/?search=startViewTransition
if (document.startViewTransition) {
await document.startViewTransition(transition);
} else {
await transition()
}
});
await transition;
window.history.pushState(undefined, null, url);
});

View File

@@ -21,7 +21,7 @@ server_fn = { path = "../../server_fn", features = [
log = "0.4.22"
simple_logger = "5.0"
serde = { version = "1.0", features = ["derive"] }
axum = { version = "0.8.1", optional = true }
axum = { version = "0.7.5", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", features = [
"fs",

View File

@@ -9,9 +9,8 @@ use server_fn::{
MultipartFormData, Postcard, Rkyv, SerdeLite, StreamingText,
TextStream,
},
error::{FromServerFnError, IntoAppError, ServerFnErrorErr},
request::{browser::BrowserRequest, ClientReq, Req},
response::{browser::BrowserResponse, ClientRes, TryRes},
response::{browser::BrowserResponse, ClientRes, Res},
};
use std::future::Future;
#[cfg(feature = "ssr")]
@@ -653,72 +652,32 @@ pub fn FileWatcher() -> impl IntoView {
/// implementations if you'd like. However, it's much lighter weight to use something like `strum`
/// simply to generate those trait implementations.
#[server]
pub async fn ascii_uppercase(text: String) -> Result<String, MyErrors> {
other_error()?;
Ok(ascii_uppercase_inner(text)?)
}
pub fn other_error() -> Result<(), String> {
Ok(())
}
pub fn ascii_uppercase_inner(text: String) -> Result<String, InvalidArgument> {
pub async fn ascii_uppercase(
text: String,
) -> Result<String, ServerFnError<InvalidArgument>> {
if text.len() < 5 {
Err(InvalidArgument::TooShort)
Err(InvalidArgument::TooShort.into())
} else if text.len() > 15 {
Err(InvalidArgument::TooLong)
Err(InvalidArgument::TooLong.into())
} else if text.is_ascii() {
Ok(text.to_ascii_uppercase())
} else {
Err(InvalidArgument::NotAscii)
Err(InvalidArgument::NotAscii.into())
}
}
#[server]
pub async fn ascii_uppercase_classic(
text: String,
) -> Result<String, ServerFnError<InvalidArgument>> {
Ok(ascii_uppercase_inner(text)?)
}
// The EnumString and Display derive macros are provided by strum
#[derive(Debug, Clone, Display, EnumString, Serialize, Deserialize)]
#[derive(Debug, Clone, EnumString, Display)]
pub enum InvalidArgument {
TooShort,
TooLong,
NotAscii,
}
#[derive(Debug, Clone, Display, Serialize, Deserialize)]
pub enum MyErrors {
InvalidArgument(InvalidArgument),
ServerFnError(ServerFnErrorErr),
Other(String),
}
impl From<InvalidArgument> for MyErrors {
fn from(value: InvalidArgument) -> Self {
MyErrors::InvalidArgument(value)
}
}
impl From<String> for MyErrors {
fn from(value: String) -> Self {
MyErrors::Other(value)
}
}
impl FromServerFnError for MyErrors {
fn from_server_fn_error(value: ServerFnErrorErr) -> Self {
MyErrors::ServerFnError(value)
}
}
#[component]
pub fn CustomErrorTypes() -> impl IntoView {
let input_ref = NodeRef::<Input>::new();
let (result, set_result) = signal(None);
let (result_classic, set_result_classic) = signal(None);
view! {
<h3>Using custom error types</h3>
@@ -733,17 +692,14 @@ pub fn CustomErrorTypes() -> impl IntoView {
<button on:click=move |_| {
let value = input_ref.get().unwrap().value();
spawn_local(async move {
let data = ascii_uppercase(value.clone()).await;
let data_classic = ascii_uppercase_classic(value).await;
let data = ascii_uppercase(value).await;
set_result.set(Some(data));
set_result_classic.set(Some(data_classic));
});
}>
"Submit"
</button>
<p>{move || format!("{:?}", result.get())}</p>
<p>{move || format!("{:?}", result_classic.get())}</p>
}
}
@@ -770,12 +726,14 @@ impl<T, Request, Err> IntoReq<Toml, Request, Err> for TomlEncoded<T>
where
Request: ClientReq<Err>,
T: Serialize,
Err: FromServerFnError,
{
fn into_req(self, path: &str, accepts: &str) -> Result<Request, Err> {
let data = toml::to_string(&self.0).map_err(|e| {
ServerFnErrorErr::Serialization(e.to_string()).into_app_error()
})?;
fn into_req(
self,
path: &str,
accepts: &str,
) -> Result<Request, ServerFnError<Err>> {
let data = toml::to_string(&self.0)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
Request::try_new_post(path, Toml::CONTENT_TYPE, accepts, data)
}
}
@@ -784,26 +742,23 @@ impl<T, Request, Err> FromReq<Toml, Request, Err> for TomlEncoded<T>
where
Request: Req<Err> + Send,
T: DeserializeOwned,
Err: FromServerFnError,
{
async fn from_req(req: Request) -> Result<Self, Err> {
async fn from_req(req: Request) -> Result<Self, ServerFnError<Err>> {
let string_data = req.try_into_string().await?;
toml::from_str::<T>(&string_data)
.map(TomlEncoded)
.map_err(|e| ServerFnErrorErr::Args(e.to_string()).into_app_error())
.map_err(|e| ServerFnError::Args(e.to_string()))
}
}
impl<T, Response, Err> IntoRes<Toml, Response, Err> for TomlEncoded<T>
where
Response: TryRes<Err>,
Response: Res<Err>,
T: Serialize + Send,
Err: FromServerFnError,
{
async fn into_res(self) -> Result<Response, Err> {
let data = toml::to_string(&self.0).map_err(|e| {
ServerFnErrorErr::Serialization(e.to_string()).into_app_error()
})?;
async fn into_res(self) -> Result<Response, ServerFnError<Err>> {
let data = toml::to_string(&self.0)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
Response::try_from_string(Toml::CONTENT_TYPE, data)
}
}
@@ -812,13 +767,12 @@ impl<T, Response, Err> FromRes<Toml, Response, Err> for TomlEncoded<T>
where
Response: ClientRes<Err> + Send,
T: DeserializeOwned,
Err: FromServerFnError,
{
async fn from_res(res: Response) -> Result<Self, Err> {
async fn from_res(res: Response) -> Result<Self, ServerFnError<Err>> {
let data = res.try_into_string().await?;
toml::from_str(&data).map(TomlEncoded).map_err(|e| {
ServerFnErrorErr::Deserialization(e.to_string()).into_app_error()
})
toml::from_str(&data)
.map(TomlEncoded)
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
}
}
@@ -881,10 +835,7 @@ pub fn CustomClientExample() -> impl IntoView {
pub struct CustomClient;
// Implement the `Client` trait for it.
impl<E> Client<E> for CustomClient
where
E: FromServerFnError,
{
impl<CustErr> Client<CustErr> for CustomClient {
// BrowserRequest and BrowserResponse are the defaults used by other server functions.
// They are wrappers for the underlying Web Fetch API types.
type Request = BrowserRequest;
@@ -893,7 +844,8 @@ pub fn CustomClientExample() -> impl IntoView {
// Our custom `send()` implementation does all the work.
fn send(
req: Self::Request,
) -> impl Future<Output = Result<Self::Response, E>> + Send {
) -> impl Future<Output = Result<Self::Response, ServerFnError<CustErr>>>
+ Send {
// BrowserRequest derefs to the underlying Request type from gloo-net,
// so we can get access to the headers here
let headers = req.headers();

View File

@@ -20,7 +20,7 @@ leptos_router = { path = "../../router" }
log = "0.4.22"
serde = { version = "1.0", features = ["derive"] }
thiserror = "1.0"
axum = { version = "0.8.1", optional = true }
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 = [

View File

@@ -18,7 +18,7 @@ leptos_router = { path = "../../router" }
log = "0.4.22"
serde = { version = "1.0", features = ["derive"] }
thiserror = "1.0"
axum = { version = "0.8.1", optional = true }
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 = [
@@ -45,7 +45,7 @@ ssr = [
"dep:leptos_axum",
"leptos_router/ssr",
"dep:notify",
"dep:http",
"dep:http"
]
[profile.release]

View File

@@ -10,7 +10,7 @@ crate-type = ["cdylib", "rlib"]
actix-files = { version = "0.6.6", optional = true }
actix-web = { version = "4.8", optional = true, features = ["macros"] }
console_error_panic_hook = "0.1.7"
js-sys = { version = "0.3.72" }
js-sys = { version = "0.3.70", optional = true }
leptos = { path = "../../leptos" }
leptos_actix = { path = "../../integrations/actix", optional = true }
leptos_router = { path = "../../router" }
@@ -21,7 +21,7 @@ tokio = { version = "1.39", features = ["time", "rt"], optional = true }
[features]
hydrate = [
"dep:js-sys",
"leptos/hydrate",
]
ssr = [

View File

@@ -1,121 +0,0 @@
@check_aria_current
Feature: Check aria-current being applied to make links bolded
Background:
Given I see the app
Scenario: Should see the base case working
Then I see the Out-of-Order link being bolded
And I see the following links being bolded
| Out-of-Order |
| Nested |
And I see the In-Order link not being bolded
And I see the following links not being bolded
| In-Order |
| Single |
Scenario: Should see client-side render the correct bolded links
When I select the link In-Order
And I select the link Single
Then I see the following links being bolded
| In-Order |
| Single |
And I see the following links not being bolded
| Out-of-Order |
| Nested |
Scenario: Should see server-side render the correct bolded links
When I select the link In-Order
And I select the link Single
And I reload the page
Then I see the following links being bolded
| In-Order |
| Single |
And I see the following links not being bolded
| Out-of-Order |
| Nested |
Scenario: Check that the base nested route links are working
When I select the link Instrumented
Then I see the Instrumented link being bolded
And I see the Item Listing link not being bolded
Scenario: Should see going deep down into nested routes bold links
When I select the link Instrumented
And I select the link Target 421
Then I see the following links being bolded
| Instrumented |
| Item Listing |
| Target 4## |
| Target 42# |
| Target 421 |
| field1 |
Scenario: Should see going deep down into nested routes in SSR bold links
When I select the link Instrumented
And I select the link Target 421
And I reload the page
Then I see the following links being bolded
| Instrumented |
| Item Listing |
| Target 4## |
| Target 42# |
| Target 421 |
| field1 |
Scenario: Going deep down navigate around nested links bold correctly
When I select the link Instrumented
And I select the link Target 421
And I select the link Inspect path2/field3
Then I see the following links being bolded
| Instrumented |
| Item Listing |
| Target 4## |
| Target 42# |
| field3 |
And I see the following links not being bolded
| Target 421 |
| field1 |
Scenario: Going deep down navigate around nested links bold correctly, SSR
When I select the link Instrumented
And I select the link Target 421
And I select the link Inspect path2/field3
And I reload the page
Then I see the following links being bolded
| Instrumented |
| Item Listing |
| Target 4## |
| Target 42# |
| field3 |
And I see the following links not being bolded
| Target 421 |
| field1 |
Scenario: Going deep down back out nested routes reset bolded states
When I select the link Instrumented
And I select the link Target 421
And I select the link Counters
Then I see the following links being bolded
| Instrumented |
| Counters |
And I see the following links not being bolded
| Item Listing |
| Target 4## |
| Target 42# |
| Target 421 |
Scenario: Going deep down back out nested routes reset bolded states, SSR
When I select the link Instrumented
And I select the link Target 421
And I select the link Counters
And I reload the page
Then I see the following links being bolded
| Instrumented |
| Counters |
And I see the following links not being bolded
| Item Listing |
| Target 4## |
| Target 42# |
| Target 421 |

View File

@@ -81,20 +81,3 @@ pub async fn instrumented_counts(
Ok(())
}
pub async fn link_text_is_aria_current(client: &Client, text: &str) -> Result<()> {
let link = find::link_with_text(client, text).await?;
link.attr("aria-current").await?
.expect(format!("aria-current missing for {text}").as_str());
Ok(())
}
pub async fn link_text_is_not_aria_current(client: &Client, text: &str) -> Result<()> {
let link = find::link_with_text(client, text).await?;
link.attr("aria-current").await?
.map(|_| anyhow::bail!("aria-current mistakenly set for {text}"))
.unwrap_or(Ok(()))
}

View File

@@ -124,12 +124,3 @@ async fn component_message(client: &Client, id: &str) -> Result<String> {
Ok(text)
}
pub async fn link_with_text(client: &Client, text: &str) -> Result<Element> {
let link = client
.wait()
.for_element(Locator::LinkText(text))
.await
.expect(format!("Link not found by `{}`", text).as_str());
Ok(link)
}

View File

@@ -80,58 +80,6 @@ async fn i_see_the_second_count_is(
Ok(())
}
#[then(regex = r"^I see the (.*) link being bolded$")]
async fn i_see_the_link_being_bolded(
world: &mut AppWorld,
text: String,
) -> Result<()> {
let client = &world.client;
check::link_text_is_aria_current(client, &text).await?;
Ok(())
}
#[then(expr = "I see the following links being bolded")]
async fn i_see_the_following_links_being_bolded(
world: &mut AppWorld,
step: &Step,
) -> Result<()> {
let client = &world.client;
if let Some(table) = step.table.as_ref() {
for row in table.rows.iter() {
check::link_text_is_aria_current(client, &row[0]).await?;
}
}
Ok(())
}
#[then(regex = r"^I see the (.*) link not being bolded$")]
async fn i_see_the_link_being_not_bolded(
world: &mut AppWorld,
text: String,
) -> Result<()> {
let client = &world.client;
check::link_text_is_not_aria_current(client, &text).await?;
Ok(())
}
#[then(expr = "I see the following links not being bolded")]
async fn i_see_the_following_links_not_being_bolded(
world: &mut AppWorld,
step: &Step,
) -> Result<()> {
let client = &world.client;
if let Some(table) = step.table.as_ref() {
for row in table.rows.iter() {
check::link_text_is_not_aria_current(client, &row[0]).await?;
}
}
Ok(())
}
#[then(expr = "I see the following counters under section")]
#[then(expr = "the following counters under section")]
async fn i_see_the_following_counters_under_section(

View File

@@ -7,7 +7,7 @@ edition = "2021"
crate-type = ["cdylib", "rlib"]
[dependencies]
axum = { version = "0.8.1", optional = true }
axum = { version = "0.7.5", optional = true }
console_error_panic_hook = "0.1.7"
leptos = { path = "../../leptos" }
leptos_meta = { path = "../../meta" }

View File

@@ -16,7 +16,7 @@ leptos_axum = { path = "../../integrations/axum", optional = true }
log = "0.4.22"
simple_logger = "5.0"
serde = { version = "1.0", features = ["derive"] }
axum = { version = "0.8.1", optional = true }
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 = ["full"], optional = true }

View File

@@ -1,4 +1,4 @@
#[cfg(feature = "ssr")]
use crate::todo::*;
use axum::{
body::Body,
extract::Path,
@@ -8,9 +8,10 @@ use axum::{
Router,
};
use leptos::prelude::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use todo_app_sqlite_axum::*;
//Define a handler to test extractor with state
#[cfg(feature = "ssr")]
async fn custom_handler(
Path(id): Path<String>,
req: Request<Body>,
@@ -19,16 +20,14 @@ async fn custom_handler(
move || {
provide_context(id.clone());
},
todo::TodoApp,
TodoApp,
);
handler(req).await.into_response()
}
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use crate::todo::{ssr::db, *};
use leptos_axum::{generate_route_list, LeptosRoutes};
use crate::todo::ssr::db;
simple_logger::init_with_level(log::Level::Error)
.expect("couldn't initialize logging");
@@ -46,7 +45,7 @@ async fn main() {
// build our application with a route
let app = Router::new()
.route("/special/{id}", get(custom_handler))
.route("/special/:id", get(custom_handler))
.leptos_routes(&leptos_options, routes, {
let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone())
@@ -62,12 +61,3 @@ async fn main() {
.await
.unwrap();
}
#[cfg(not(feature = "ssr"))]
pub fn main() {
use leptos::mount::mount_to_body;
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(todo::TodoApp);
}

View File

@@ -15,7 +15,7 @@ leptos_meta = { path = "../../meta" }
leptos_router = { path = "../../router" }
leptos_integration_utils = { path = "../../integrations/utils", optional = true }
serde = { version = "1.0", features = ["derive"] }
axum = { version = "0.8.1", optional = true }
axum = { version = "0.7.5", optional = true }
tower = { version = "0.5.1", features = ["util"], optional = true }
tower-http = { version = "0.6.1", features = ["fs"], optional = true }
tokio = { version = "1.39", features = ["full"], optional = true }

View File

@@ -34,7 +34,7 @@ async fn main() {
// here, we're not actually doing server side rendering, so we set up a manual
// handler for the server fns
// this should include a get() handler if you have any GetUrl-based server fns
.route("/api/{*fn_name}", post(leptos_axum::handle_server_fns))
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
.fallback(file_or_index_handler)
.with_state(leptos_options);

View File

@@ -4,6 +4,25 @@ use leptos::prelude::*;
use serde::{Deserialize, Serialize};
use server_fn::ServerFnError;
pub fn shell(leptos_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=leptos_options.clone() />
<HydrationScripts options=leptos_options.clone()/>
<link rel="stylesheet" id="leptos" href="/pkg/todo_app_sqlite_csr.css"/>
<link rel="shortcut icon" type="image/ico" href="/favicon.ico"/>
</head>
<body>
<TodoApp/>
</body>
</html>
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
pub struct Todo {

View File

@@ -1,6 +1,6 @@
[package]
name = "hydration_context"
version = "0.2.1"
version = "0.2.0-rc1"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
@@ -14,8 +14,8 @@ throw_error = { workspace = true }
or_poisoned = { workspace = true }
futures = "0.3.31"
serde = { version = "1.0", features = ["derive"] }
wasm-bindgen = { version = "0.2.97", optional = true }
js-sys = { version = "0.3.74", optional = true }
wasm-bindgen = { version = "0.2.95", optional = true }
js-sys = { version = "0.3.72", optional = true }
once_cell = "1.20"
pin-project-lite = "0.2.15"
@@ -25,6 +25,3 @@ browser = ["dep:wasm-bindgen", "dep:js-sys"]
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }

View File

@@ -1,8 +1,3 @@
// #[wasm_bindgen(thread_local)] is deprecated in wasm-bindgen 0.2.96
// but the replacement is also only shipped in that version
// as a result, we'll just allow deprecated for now
#![allow(deprecated)]
use super::{SerializedDataId, SharedContext};
use crate::{PinnedFuture, PinnedStream};
use core::fmt::Debug;

View File

@@ -83,14 +83,15 @@ pub trait SharedContext: Debug {
/// Reads the current value of some data from the shared context, if it has been
/// sent from the server. This returns the serialized data as a `String` that should
/// be deserialized.
/// be deserialized using [`Serializable::de`].
///
/// On the server and in client-side rendered implementations, this should
/// always return [`None`].
fn read_data(&self, id: &SerializedDataId) -> Option<String>;
/// Returns a [`Future`] that resolves with a `String` that should
/// be deserialized once the given piece of server data has resolved.
/// be deserialized using [`Serializable::de`] once the given piece of server
/// data has resolved.
///
/// On the server and in client-side rendered implementations, this should
/// return a [`Future`] that is immediately ready with [`None`].
@@ -147,8 +148,8 @@ pub trait SharedContext: Debug {
/// Adds a `Future` to the set of “blocking resources” that should prevent the servers
/// response stream from beginning until all are resolved. The `Future` returned by
/// blocking resources will not resolve until every `Future` added by this method
/// has resolved.
/// [`blocking_resources`](Self::blocking_resources) will not resolve until every `Future`
/// added by this method has resolved.
///
/// In browser implementations, this should be a no-op.
fn defer_stream(&self, wait_for: PinnedFuture<()>);

View File

@@ -18,7 +18,7 @@ hydration_context = { workspace = true }
leptos = { workspace = true, features = ["nonce", "ssr"] }
leptos_integration_utils = { workspace = true }
leptos_macro = { workspace = true, features = ["actix"] }
leptos_meta = { workspace = true, features = ["nonce"] }
leptos_meta = { workspace = true }
leptos_router = { workspace = true, features = ["ssr"] }
server_fn = { workspace = true, features = ["actix"] }
serde_json = "1.0"

View File

@@ -1,5 +1,4 @@
#![forbid(unsafe_code)]
#![deny(missing_docs)]
//! Provides functions to easily integrate Leptos with Actix.
//!
@@ -10,6 +9,7 @@
use actix_files::NamedFile;
use actix_http::header::{HeaderName, HeaderValue, ACCEPT, LOCATION, REFERER};
use actix_web::{
body::BoxBody,
dev::{ServiceFactory, ServiceRequest},
http::header,
test,
@@ -56,10 +56,8 @@ use std::{
/// Typically contained inside of a ResponseOptions. Setting this is useful for cookies and custom responses.
#[derive(Debug, Clone, Default)]
pub struct ResponseParts {
/// If provided, this will overwrite any other status code for this response.
pub status: Option<StatusCode>,
/// The map of headers that should be added to the response.
pub headers: header::HeaderMap,
pub status: Option<StatusCode>,
}
impl ResponseParts {
@@ -82,18 +80,16 @@ impl ResponseParts {
}
}
/// A wrapper for an Actix [`HttpRequest`] that allows it to be used in an
/// A wrapper for an Actix [`HttpRequest`](actix_web::HttpRequest) that allows it to be used in an
/// `Send`/`Sync` setting like Leptos's Context API.
#[derive(Debug, Clone)]
pub struct Request(SendWrapper<HttpRequest>);
impl Request {
/// Wraps an existing Actix request.
pub fn new(req: &HttpRequest) -> Self {
Self(SendWrapper::new(req.clone()))
}
/// Consumes the wrapper and returns the inner Actix request.
pub fn into_inner(self) -> HttpRequest {
self.0.take()
}
@@ -303,7 +299,7 @@ pub fn redirect(path: &str) {
/// ## Provided Context Types
/// This function always provides context values including the following types:
/// - [ResponseOptions]
/// - [Request]
/// - [HttpRequest](actix_web::HttpRequest)
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
@@ -330,7 +326,7 @@ pub fn handle_server_fns() -> Route {
/// ## Provided Context Types
/// This function always provides context values including the following types:
/// - [ResponseOptions]
/// - [Request]
/// - [HttpRequest](actix_web::HttpRequest)
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
@@ -369,13 +365,14 @@ pub fn handle_server_fns_with_context(
// actually run the server fn
let mut res = ActixResponse(
service
.0
.run(ActixRequest::from((req, payload)))
.await
.take(),
);
// if it accepts text/html (i.e., is a plain form post) and doesn't already have a
// Location set, then redirect to the Referer
// it it accepts text/html (i.e., is a plain form post) and doesn't already have a
// Location set, then redirect to to Referer
if accepts_html {
if let Some(referrer) = referrer {
let has_location =
@@ -389,20 +386,7 @@ pub fn handle_server_fns_with_context(
}
}
// the Location header may have been set to Referer, so any redirection by the
// user must overwrite it
{
let mut res_options = res_options.0.write();
let headers = res.0.headers_mut();
for location in
res_options.headers.remove(header::LOCATION)
{
headers.insert(header::LOCATION, location);
}
}
// apply status code and headers if user changed them
// apply status code and headers if used changed them
res.extend_response(&res_options);
res.0
})
@@ -431,6 +415,12 @@ pub fn handle_server_fns_with_context(
/// will include fallback content for any `<Suspense/>` nodes, and be immediately interactive,
/// but requires some client-side JavaScript.
///
/// The provides a [MetaContext] and a [RouterIntegrationContext] to apps context before
/// rendering it, and includes any meta tags injected using [leptos_meta].
///
/// The HTML stream is rendered using [render_to_stream](leptos::ssr::render_to_stream), and
/// includes everything described in the documentation for that function.
///
/// This can then be set up at an appropriate route in your application:
/// ```
/// use actix_web::{App, HttpServer};
@@ -469,8 +459,9 @@ pub fn handle_server_fns_with_context(
/// ## Provided Context Types
/// This function always provides context values including the following types:
/// - [ResponseOptions]
/// - [Request]
/// - [HttpRequest](actix_web::HttpRequest)
/// - [MetaContext](leptos_meta::MetaContext)
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
@@ -490,6 +481,13 @@ where
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve before
/// sending down its HTML. The app will become interactive once it has fully loaded.
///
/// The provides a [MetaContext] and a [RouterIntegrationContext] to apps context before
/// rendering it, and includes any meta tags injected using [leptos_meta].
///
/// The HTML stream is rendered using
/// [render_to_stream_in_order](leptos::ssr::render_to_stream_in_order),
/// and includes everything described in the documentation for that function.
///
/// This can then be set up at an appropriate route in your application:
/// ```
/// use actix_web::{App, HttpServer};
@@ -531,7 +529,9 @@ where
/// ## Provided Context Types
/// This function always provides context values including the following types:
/// - [ResponseOptions]
/// - [Request]
/// - [HttpRequest](actix_web::HttpRequest)
/// - [MetaContext](leptos_meta::MetaContext)
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
@@ -548,7 +548,13 @@ where
/// Returns an Actix [struct@Route](actix_web::Route) that listens for a `GET` request and tries
/// to route it using [leptos_router], asynchronously rendering an HTML page after all
/// `async` resources have loaded.
/// `async` [Resource](leptos::Resource)s have loaded.
///
/// The provides a [MetaContext] and a [RouterIntegrationContext] to the apps context before
/// rendering it, and includes any meta tags injected using [leptos_meta].
///
/// The HTML stream is rendered using [render_to_string_async](leptos::ssr::render_to_string_async), and
/// includes everything described in the documentation for that function.
///
/// This can then be set up at an appropriate route in your application:
/// ```
@@ -588,7 +594,9 @@ where
/// ## Provided Context Types
/// This function always provides context values including the following types:
/// - [ResponseOptions]
/// - [Request]
/// - [HttpRequest](actix_web::HttpRequest)
/// - [MetaContext](leptos_meta::MetaContext)
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
@@ -612,7 +620,9 @@ where
/// ## Provided Context Types
/// This function always provides context values including the following types:
/// - [ResponseOptions]
/// - [Request]
/// - [HttpRequest](actix_web::HttpRequest)
/// - [MetaContext](leptos_meta::MetaContext)
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
@@ -647,7 +657,9 @@ where
/// ## Provided Context Types
/// This function always provides context values including the following types:
/// - [ResponseOptions]
/// - [Request]
/// - [HttpRequest](actix_web::HttpRequest)
/// - [MetaContext](leptos_meta::MetaContext)
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
@@ -679,8 +691,9 @@ where
/// ## Provided Context Types
/// This function always provides context values including the following types:
/// - [ResponseOptions]
/// - [Request]
/// - [HttpRequest](actix_web::HttpRequest)
/// - [MetaContext](leptos_meta::MetaContext)
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
@@ -703,7 +716,7 @@ where
/// Returns an Actix [struct@Route](actix_web::Route) that listens for a `GET` request and tries
/// to route it using [leptos_router], asynchronously serving the page once all `async`
/// resources have loaded.
/// [Resource](leptos::Resource)s have loaded.
///
/// This function allows you to provide additional information to Leptos for your route.
/// It could be used to pass in Path Info, Connection Info, or anything your heart desires.
@@ -711,7 +724,9 @@ where
/// ## Provided Context Types
/// This function always provides context values including the following types:
/// - [ResponseOptions]
/// - [Request]
/// - [HttpRequest](actix_web::HttpRequest)
/// - [MetaContext](leptos_meta::MetaContext)
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
@@ -1304,12 +1319,14 @@ where
web::get().to(handler)
}
pub enum DataResponse<T> {
Data(T),
Response(actix_web::dev::Response<BoxBody>),
}
/// 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 {
/// Adds routes to the Axum router that have either
/// 1) been generated by `leptos_router`, or
/// 2) handle a server function.
fn leptos_routes<IV>(
self,
paths: Vec<ActixRouteListing>,
@@ -1318,12 +1335,6 @@ pub trait LeptosRoutes {
where
IV: IntoView + 'static;
/// Adds routes to the Axum router that have either
/// 1) been generated by `leptos_router`, or
/// 2) handle a server function.
///
/// Runs `additional_context` to provide additional data to the reactive system via context,
/// when handling a route.
fn leptos_routes_with_context<IV>(
self,
paths: Vec<ActixRouteListing>,

View File

@@ -11,7 +11,7 @@ edition.workspace = true
[dependencies]
any_spawner = { workspace = true, features = ["tokio"] }
hydration_context = { workspace = true }
axum = { version = "0.8.1", default-features = false, features = [
axum = { version = "0.7.7", default-features = false, features = [
"matched-path",
] }
dashmap = "6"
@@ -19,18 +19,18 @@ futures = "0.3.31"
leptos = { workspace = true, features = ["nonce", "ssr"] }
server_fn = { workspace = true, features = ["axum-no-default"] }
leptos_macro = { workspace = true, features = ["axum"] }
leptos_meta = { workspace = true, features = ["ssr", "nonce"] }
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"
tokio = { version = "1.41", default-features = false }
tower = { version = "0.5.1", features = ["util"] }
tower-http = "0.6.2"
tracing = { version = "0.1.41", optional = true }
tower-http = "0.6.1"
tracing = { version = "0.1.40", optional = true }
[dev-dependencies]
axum = "0.8.1"
axum = "0.7.7"
tokio = { version = "1.41", features = ["net", "rt-multi-thread"] }
[features]

View File

@@ -1,6 +1,4 @@
#![forbid(unsafe_code)]
#![deny(missing_docs)]
//! Provides functions to easily integrate Leptos with Axum.
//!
//! ## JS Fetch Integration
@@ -16,6 +14,7 @@
//! - `default`: supports running in a typical native Tokio/Axum environment
//! - `wasm`: with `default-features = false`, supports running in a JS Fetch-based
//! environment
//! - `experimental-islands`: activates Leptos [islands mode](https://leptos-rs.github.io/leptos/islands.html)
//!
//! ### Important Note
//! Prior to 0.5, using `default-features = false` on `leptos_axum` simply did nothing. Now, it actively
@@ -64,9 +63,10 @@ use leptos_meta::ServerMetaContext;
#[cfg(feature = "default")]
use leptos_router::static_routes::ResolvedStaticPath;
use leptos_router::{
components::provide_server_redirect, location::RequestUrl,
static_routes::RegenerationFn, ExpandOptionals, PathSegment, RouteList,
RouteListing, SsrMode,
components::provide_server_redirect,
location::RequestUrl,
static_routes::{RegenerationFn, StaticParamsMap},
ExpandOptionals, PathSegment, RouteList, RouteListing, SsrMode,
};
#[cfg(feature = "default")]
use once_cell::sync::Lazy;
@@ -85,9 +85,7 @@ use tower_http::services::ServeDir;
/// Typically contained inside of a ResponseOptions. Setting this is useful for cookies and custom responses.
#[derive(Debug, Clone, Default)]
pub struct ResponseParts {
/// If provided, this will overwrite any other status code for this response.
pub status: Option<StatusCode>,
/// The map of headers that should be added to the response.
pub headers: HeaderMap,
}
@@ -196,9 +194,9 @@ impl ExtendResponse for AxumResponse {
/// 2. A server function that is called from WASM running in the client (e.g., a dispatched action
/// or a spawned `Future`).
/// 3. A `<form>` submitted to the server function endpoint using default browser APIs (often due
/// to using [`ActionForm`] without JS/WASM present.)
/// to using [`ActionForm`](leptos::form::ActionForm) without JS/WASM present.)
///
/// Using it with a non-blocking [`Resource`] will not work if you are using streaming rendering,
/// Using it with a non-blocking [`Resource`](leptos::server::Resource) will not work if you are using streaming rendering,
/// as the response's headers will already have been sent by the time the server function calls `redirect()`.
///
/// ### Implementation
@@ -368,6 +366,8 @@ async fn handle_server_fns_inner(
additional_context: impl Fn() + 'static + Clone + Send,
req: Request<Body>,
) -> impl IntoResponse {
use server_fn::middleware::Service;
let method = req.method().clone();
let path = req.uri().path().to_string();
let (req, parts) = generate_request_and_parts(req);
@@ -396,8 +396,8 @@ async fn handle_server_fns_inner(
// actually run the server fn
let mut res = AxumResponse(service.run(req).await);
// if it accepts text/html (i.e., is a plain form post) and doesn't already have a
// Location set, then redirect to the Referer
// it it accepts text/html (i.e., is a plain form post) and doesn't already have a
// Location set, then redirect to to Referer
if accepts_html {
if let Some(referrer) = referrer {
let has_location =
@@ -409,7 +409,7 @@ async fn handle_server_fns_inner(
}
}
// apply status code and headers if user changed them
// apply status code and headers if used changed them
res.extend_response(&res_options);
Ok(res.0)
})
@@ -432,13 +432,18 @@ async fn handle_server_fns_inner(
.expect("could not build Response")
}
/// A stream of bytes of HTML.
pub type PinnedHtmlStream =
Pin<Box<dyn Stream<Item = io::Result<Bytes>> + Send>>;
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
/// to route it using [leptos_router], serving an HTML stream of your application.
///
/// The provides a [MetaContext] and a [RouterIntegrationContext] to apps context before
/// rendering it, and includes any meta tags injected using [leptos_meta].
///
/// The HTML stream is rendered using [render_to_stream](leptos::ssr::render_to_stream), and
/// includes everything described in the documentation for that function.
///
/// This can then be set up at an appropriate route in your application:
/// ```
/// use axum::{handler::Handler, Router};
@@ -476,13 +481,14 @@ pub type PinnedHtmlStream =
/// This function always provides context values including the following types:
/// - [`Parts`]
/// - [`ResponseOptions`]
/// - [`ServerMetaContext`]
/// - [`ServerMetaContext`](leptos_meta::ServerMetaContext)
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream<IV>(
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
@@ -506,7 +512,7 @@ where
)]
pub fn render_route<S, IV>(
paths: Vec<AxumRouteListing>,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> impl Fn(
State<S>,
Request<Body>,
@@ -527,6 +533,12 @@ where
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve before
/// sending down its HTML. The app will become interactive once it has fully loaded.
///
/// The provides a [MetaContext] and a [RouterIntegrationContext] to apps context before
/// rendering it, and includes any meta tags injected using [leptos_meta].
///
/// The HTML stream is rendered using [render_to_stream_in_order], and includes everything described in
/// the documentation for that function.
///
/// This can then be set up at an appropriate route in your application:
/// ```
/// use axum::{handler::Handler, Router};
@@ -564,13 +576,14 @@ where
/// This function always provides context values including the following types:
/// - [`Parts`]
/// - [`ResponseOptions`]
/// - [`ServerMetaContext`]
/// - [`ServerMetaContext`](leptos_meta::ServerMetaContext)
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream_in_order<IV>(
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
@@ -617,20 +630,20 @@ where
/// This function always provides context values including the following types:
/// - [`Parts`]
/// - [`ResponseOptions`]
/// - [`ServerMetaContext`]
/// - [`ServerMetaContext`](leptos_meta::ServerMetaContext)
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream_with_context<IV>(
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
+ Clone
+ Send
+ Sync
+ 'static
where
IV: IntoView + 'static,
@@ -653,8 +666,8 @@ where
)]
pub fn render_route_with_context<S, IV>(
paths: Vec<AxumRouteListing>,
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> impl Fn(
State<S>,
Request<Body>,
@@ -749,21 +762,21 @@ where
/// This function always provides context values including the following types:
/// - [`Parts`]
/// - [`ResponseOptions`]
/// - [`ServerMetaContext`]
/// - [`ServerMetaContext`](leptos_meta::ServerMetaContext)
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream_with_context_and_replace_blocks<IV>(
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
replace_blocks: bool,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
+ Clone
+ Send
+ Sync
+ 'static
where
IV: IntoView + 'static,
@@ -817,14 +830,15 @@ where
/// This function always provides context values including the following types:
/// - [`Parts`]
/// - [`ResponseOptions`]
/// - [`ServerMetaContext`]
/// - [`ServerMetaContext`](leptos_meta::ServerMetaContext)
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream_in_order_with_context<IV>(
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
@@ -847,17 +861,13 @@ where
}
fn handle_response<IV>(
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
stream_builder: fn(
IV,
BoxedFnOnce<PinnedStream<String>>,
) -> PinnedFuture<PinnedStream<String>>,
) -> impl Fn(Request<Body>) -> PinnedFuture<Response<Body>>
+ Clone
+ Send
+ Sync
+ 'static
) -> impl Fn(Request<Body>) -> PinnedFuture<Response<Body>> + Clone + Send + 'static
where
IV: IntoView + 'static,
{
@@ -938,7 +948,13 @@ fn provide_contexts(
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
/// to route it using [leptos_router], asynchronously rendering an HTML page after all
/// `async` resources have loaded.
/// `async` [Resource](leptos::Resource)s have loaded.
///
/// The provides a [MetaContext] and a [RouterIntegrationContext] to apps context before
/// rendering it, and includes any meta tags injected using [leptos_meta].
///
/// The HTML stream is rendered using [render_to_string_async], and includes everything described in
/// the documentation for that function.
///
/// This can then be set up at an appropriate route in your application:
/// ```
@@ -978,13 +994,14 @@ fn provide_contexts(
/// This function always provides context values including the following types:
/// - [`Parts`]
/// - [`ResponseOptions`]
/// - [`ServerMetaContext`]
/// - [`ServerMetaContext`](leptos_meta::ServerMetaContext)
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_async<IV>(
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
@@ -999,7 +1016,7 @@ where
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
/// to route it using [leptos_router], asynchronously rendering an HTML page after all
/// `async` resources have loaded.
/// `async` [Resource](leptos::Resource)s have loaded.
///
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
@@ -1032,14 +1049,15 @@ where
/// This function always provides context values including the following types:
/// - [`Parts`]
/// - [`ResponseOptions`]
/// - [`ServerMetaContext`]
/// - [`ServerMetaContext`](leptos_meta::ServerMetaContext)
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_async_stream_with_context<IV>(
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
@@ -1066,7 +1084,7 @@ where
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
/// to route it using [leptos_router], asynchronously rendering an HTML page after all
/// `async` resources have loaded.
/// `async` [Resource](leptos::Resource)s have loaded.
///
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
@@ -1099,14 +1117,15 @@ where
/// This function always provides context values including the following types:
/// - [`Parts`]
/// - [`ResponseOptions`]
/// - [`ServerMetaContext`]
/// - [`ServerMetaContext`](leptos_meta::ServerMetaContext)
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_async_with_context<IV>(
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
@@ -1188,6 +1207,32 @@ where
generate_route_list_with_exclusions_and_ssg(app_fn, excluded_routes).0
}
/// 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: StaticParamsMap,
) where
IV: IntoView + 'static,
{
todo!()
/*
let options = options.clone();
let routes = routes.to_owned();
spawn_task!(async move {
leptos_router::build_static_routes(
&options,
app_fn,
&routes,
&static_data_map,
)
.await
.expect("could not build static routes")
});*/
}
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
/// create routes in Axum's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths. Adding excluded_routes
@@ -1638,36 +1683,25 @@ where
S: Clone + Send + Sync + 'static,
LeptosOptions: FromRef<S>,
{
/// Adds routes to the Axum router that have either
/// 1) been generated by `leptos_router`, or
/// 2) handle a server function.
fn leptos_routes<IV>(
self,
options: &S,
paths: Vec<AxumRouteListing>,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static;
/// Adds routes to the Axum router that have either
/// 1) been generated by `leptos_router`, or
/// 2) handle a server function.
///
/// Runs `additional_context` to provide additional data to the reactive system via context,
/// when handling a route.
fn leptos_routes_with_context<IV>(
self,
options: &S,
paths: Vec<AxumRouteListing>,
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static;
/// Extends the Axum router with the given paths, and handles the requests with the given
/// handler.
fn leptos_routes_with_handler<H, T>(
self,
paths: Vec<AxumRouteListing>,
@@ -1694,15 +1728,12 @@ impl AxumPath for Vec<PathSegment> {
match segment {
PathSegment::Static(s) => path.push_str(s),
PathSegment::Param(s) => {
path.push('{');
path.push(':');
path.push_str(s);
path.push('}');
}
PathSegment::Splat(s) => {
path.push('{');
path.push('*');
path.push_str(s);
path.push('}');
}
PathSegment::Unit => {}
PathSegment::OptionalParam(_) => {
@@ -1734,7 +1765,7 @@ where
self,
state: &S,
paths: Vec<AxumRouteListing>,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static,
@@ -1750,8 +1781,8 @@ where
self,
state: &S,
paths: Vec<AxumRouteListing>,
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static,
@@ -1983,10 +2014,6 @@ where
.map_err(|e| ServerFnError::ServerError(format!("{e:?}")))
}
/// A reasonable handler for serving static files (like JS/WASM/CSS) and 404 errors.
///
/// This is provided as a convenience, but is a fairly simple function. If you need to adapt it,
/// simply reuse the source code of this function in your own application.
#[cfg(feature = "default")]
pub fn file_and_error_handler<S, IV>(
shell: fn(LeptosOptions) -> IV,
@@ -2000,12 +2027,12 @@ pub fn file_and_error_handler<S, IV>(
+ 'static
where
IV: IntoView + 'static,
S: Send + Sync + Clone + 'static,
S: Send + 'static,
LeptosOptions: FromRef<S>,
{
move |uri: Uri, State(state): State<S>, req: Request<Body>| {
move |uri: Uri, State(options): State<S>, req: Request<Body>| {
Box::pin(async move {
let options = LeptosOptions::from_ref(&state);
let options = LeptosOptions::from_ref(&options);
let res = get_static_file(uri, &options.site_root, req.headers());
let res = res.await.unwrap();
@@ -2013,9 +2040,7 @@ where
res.into_response()
} else {
let mut res = handle_response_inner(
move || {
provide_context(state.clone());
},
|| {},
move || shell(options),
req,
|app, chunks| {

View File

@@ -40,28 +40,15 @@ pub trait ExtendResponse: Sized {
let (owner, stream) =
build_response(app_fn, additional_context, stream_builder);
let sc = owner.shared_context().unwrap();
let stream = stream.await.ready_chunks(32).map(|n| n.join(""));
let sc = owner.shared_context().unwrap();
while let Some(pending) = sc.await_deferred() {
pending.await;
}
let mut stream = Box::pin(
meta_context.inject_meta_context(stream).await.then({
let sc = Arc::clone(&sc);
move |chunk| {
let sc = Arc::clone(&sc);
async move {
while let Some(pending) = sc.await_deferred() {
pending.await;
}
chunk
}
}
}),
);
let mut stream =
Box::pin(meta_context.inject_meta_context(stream).await);
// wait for the first chunk of the stream, then set the status and headers
let first_chunk = stream.next().await.unwrap_or_default();

View File

@@ -11,10 +11,7 @@ edition.workspace = true
[dependencies]
throw_error = { workspace = true }
any_spawner = { workspace = true, features = [
"wasm-bindgen",
"futures-executor",
] }
any_spawner = { workspace = true, features = ["wasm-bindgen", "futures-executor"] }
base64 = { version = "0.22.1", optional = true }
cfg-if = "1.0"
hydration_context = { workspace = true }
@@ -31,13 +28,9 @@ paste = "1.0"
rand = { version = "0.8.5", optional = true }
reactive_graph = { workspace = true, features = ["serde"] }
rustc-hash = "2.0"
tachys = { workspace = true, features = [
"reactive_graph",
"reactive_stores",
"oco",
] }
tachys = { workspace = true, features = ["reactive_graph", "reactive_stores", "oco"] }
thiserror = "2.0"
tracing = { version = "0.1.41", optional = true }
tracing = { version = "0.1.40", optional = true }
typed-builder = "0.20.0"
typed-builder-macro = "0.20.0"
serde = "1.0"
@@ -52,27 +45,25 @@ web-sys = { version = "0.3.72", features = [
"ShadowRootInit",
"ShadowRootMode",
] }
wasm-bindgen = "0.2.97"
wasm-bindgen = "0.2.95"
serde_qs = "0.13.0"
slotmap = "1.0"
futures = "0.3.31"
send_wrapper = "0.6.0"
getrandom = { version = "0.2", features = ["js"], optional = true }
[features]
hydration = [
"reactive_graph/hydration",
"leptos_server/hydration",
"hydration_context/browser",
"leptos_dom/hydration",
"leptos_dom/hydration"
]
csr = ["leptos_macro/csr", "reactive_graph/effects", "dep:getrandom"]
csr = ["leptos_macro/csr", "reactive_graph/effects"]
hydrate = [
"leptos_macro/hydrate",
"hydration",
"tachys/hydrate",
"reactive_graph/effects",
"dep:getrandom",
]
default-tls = ["server_fn/default-tls"]
rustls = ["server_fn/rustls"]
@@ -84,10 +75,7 @@ ssr = [
"tachys/ssr",
]
nightly = ["leptos_macro/nightly", "reactive_graph/nightly", "tachys/nightly"]
rkyv = [
"server_fn/rkyv",
"leptos_server/rkyv"
]
rkyv = ["server_fn/rkyv"]
tracing = [
"dep:tracing",
"reactive_graph/tracing",
@@ -98,10 +86,10 @@ tracing = [
]
nonce = ["base64", "rand"]
spin = ["leptos-spin-macro"]
islands = ["leptos_macro/islands", "dep:serde_json"]
experimental-islands = ["leptos_macro/experimental-islands", "dep:serde_json"]
trace-component-props = [
"leptos_macro/trace-component-props",
"leptos_dom/trace-component-props",
"leptos_dom/trace-component-props"
]
delegation = ["tachys/delegation"]
@@ -113,56 +101,23 @@ denylist = [
"rustls",
"default-tls",
"wasm-bindgen",
"rkyv", # was causing clippy issues on nightly
"rkyv", # was causing clippy issues on nightly
"trace-component-props",
"spin",
"islands",
"experimental-islands",
]
skip_feature_sets = [
[
"csr",
"ssr",
],
[
"csr",
"hydrate",
],
[
"ssr",
"hydrate",
],
[
"serde",
"serde-lite",
],
[
"serde-lite",
"miniserde",
],
[
"serde",
"miniserde",
],
[
"serde",
"rkyv",
],
[
"miniserde",
"rkyv",
],
[
"serde-lite",
"rkyv",
],
[
"default-tls",
"rustls",
],
["csr", "ssr"],
["csr", "hydrate"],
["ssr", "hydrate"],
["serde", "serde-lite"],
["serde-lite", "miniserde"],
["serde", "miniserde"],
["serde", "rkyv"],
["miniserde", "rkyv"],
["serde-lite", "rkyv"],
["default-tls", "rustls"],
]
[package.metadata.docs.rs]
rustdoc-args = ["--generate-link-to-definition"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }

View File

@@ -1,175 +0,0 @@
use crate::attr::{
any_attribute::{AnyAttribute, IntoAnyAttribute},
Attribute, NextAttribute,
};
use leptos::prelude::*;
use tachys::view::any_view::ExtraAttrsMut;
/// Function stored to build/rebuild the wrapped children when attributes are added.
type ChildBuilder<T> = dyn Fn(AnyAttribute) -> T + Send + Sync + 'static;
/// Intercepts attributes passed to your component, allowing passing them to any element.
///
/// By default, Leptos passes any attributes passed to your component (e.g. `<MyComponent
/// attr:class="some-class"/>`) to the top-level element in the view returned by your component.
/// [`AttributeInterceptor`] allows you to intercept this behavior and pass it onto any element in
/// your component instead.
///
/// Must be the top level element in your component's view.
///
/// ## Example
///
/// Any attributes passed to MyComponent will be passed to the #inner element.
///
/// ```
/// # use leptos::prelude::*;
/// use leptos::attribute_interceptor::AttributeInterceptor;
///
/// #[component]
/// pub fn MyComponent() -> impl IntoView {
/// view! {
/// <AttributeInterceptor let:attrs>
/// <div id="wrapper">
/// <div id="inner" {..attrs} />
/// </div>
/// </AttributeInterceptor>
/// }
/// }
/// ```
#[component(transparent)]
pub fn AttributeInterceptor<Chil, T>(
/// The elements that will be rendered, with the attributes this component received as a
/// parameter.
children: Chil,
) -> impl IntoView
where
Chil: Fn(AnyAttribute) -> T + Send + Sync + 'static,
T: IntoView + 'static,
{
AttributeInterceptorInner::new(children)
}
/// Wrapper to intercept attributes passed to a component so you can apply them to a different
/// element.
struct AttributeInterceptorInner<T: IntoView, A> {
children_builder: Box<ChildBuilder<T>>,
children: T,
attributes: A,
}
impl<T: IntoView> AttributeInterceptorInner<T, ()> {
/// Use this as the returned view from your component to collect the attributes that are passed
/// to your component so you can manually handle them.
pub fn new<F>(children: F) -> Self
where
F: Fn(AnyAttribute) -> T + Send + Sync + 'static,
{
let children_builder = Box::new(children);
let children = children_builder(().into_any_attr());
Self {
children_builder,
children,
attributes: (),
}
}
}
impl<T: IntoView, A: Attribute> Render for AttributeInterceptorInner<T, A> {
type State = <T as Render>::State;
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
self.children.build(extra_attrs)
}
fn rebuild(
self,
state: &mut Self::State,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
self.children.rebuild(state, extra_attrs);
}
}
impl<T: IntoView + 'static, A> AddAnyAttr for AttributeInterceptorInner<T, A>
where
A: Attribute,
{
type Output<SomeNewAttr: leptos::attr::Attribute> =
AttributeInterceptorInner<T, <<A as NextAttribute>::Output<SomeNewAttr> as Attribute>::CloneableOwned>;
fn add_any_attr<NewAttr: leptos::attr::Attribute>(
self,
attr: NewAttr,
) -> Self::Output<NewAttr>
where
Self::Output<NewAttr>: RenderHtml,
{
let attributes =
self.attributes.add_any_attr(attr).into_cloneable_owned();
let children =
(self.children_builder)(attributes.clone().into_any_attr());
AttributeInterceptorInner {
children_builder: self.children_builder,
children,
attributes,
}
}
}
impl<T: IntoView + 'static, A: Attribute> RenderHtml
for AttributeInterceptorInner<T, A>
{
type AsyncOutput = T::AsyncOutput;
type Owned = AttributeInterceptorInner<T, A::CloneableOwned>;
const MIN_LENGTH: usize = T::MIN_LENGTH;
fn dry_resolve(&mut self, extra_attrs: ExtraAttrsMut<'_>) {
self.children.dry_resolve(extra_attrs)
}
fn resolve(
self,
extra_attrs: ExtraAttrsMut<'_>,
) -> impl std::future::Future<Output = Self::AsyncOutput> + Send {
self.children.resolve(extra_attrs)
}
fn to_html_with_buf(
self,
buf: &mut String,
position: &mut leptos::tachys::view::Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
self.children.to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs,
)
}
fn hydrate<const FROM_SERVER: bool>(
self,
cursor: &leptos::tachys::hydration::Cursor,
position: &leptos::tachys::view::PositionState,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
self.children
.hydrate::<FROM_SERVER>(cursor, position, extra_attrs)
}
fn into_owned(self) -> Self::Owned {
AttributeInterceptorInner {
children_builder: self.children_builder,
children: self.children,
attributes: self.attributes.into_cloneable_owned(),
}
}
}

View File

@@ -31,32 +31,25 @@
//! *Notes*:
//! - The `render_number` prop can receive any type that implements `Fn(i32) -> String`.
//! - Callbacks are most useful when you want optional generic props.
//! - All callbacks implement the [`Callable`](leptos::callback::Callable) trait, and can be invoked with `my_callback.run(input)`.
//! - All callbacks implement the [`Callable`] trait, and can be invoked with `my_callback.run(input)`.
//! - The callback types implement [`Copy`], so they can easily be moved into and out of other closures, just like signals.
//!
//! # Types
//! This modules implements 2 callback types:
//! - [`Callback`](leptos::callback::Callback)
//! - [`UnsyncCallback`](leptos::callback::UnsyncCallback)
//! - [`Callback`]
//! - [`UnsyncCallback`]
//!
//! Use `SyncCallback` if the function is not `Sync` and `Send`.
use reactive_graph::{
owner::{LocalStorage, StoredValue},
traits::{Dispose, WithValue},
traits::WithValue,
};
use std::{fmt, rc::Rc, sync::Arc};
/// A wrapper trait for calling callbacks.
pub trait Callable<In: 'static, Out: 'static = ()> {
/// calls the callback with the specified argument.
///
/// Returns None if the callback has been disposed
fn try_run(&self, input: In) -> Option<Out>;
/// calls the callback with the specified argument.
///
/// # Panics
/// Panics if you try to run a callback that has been disposed
fn run(&self, input: In) -> Out;
}
@@ -79,12 +72,6 @@ impl<In, Out> Clone for UnsyncCallback<In, Out> {
}
}
impl<In, Out> Dispose for UnsyncCallback<In, Out> {
fn dispose(self) {
self.0.dispose();
}
}
impl<In, Out> UnsyncCallback<In, Out> {
/// Creates a new callback from the given function.
pub fn new<F>(f: F) -> UnsyncCallback<In, Out>
@@ -93,23 +80,9 @@ impl<In, Out> UnsyncCallback<In, Out> {
{
Self(StoredValue::new_local(Rc::new(f)))
}
/// Returns `true` if both callbacks wrap the same underlying function pointer.
#[inline]
pub fn matches(&self, other: &Self) -> bool {
self.0.with_value(|self_value| {
other
.0
.with_value(|other_value| Rc::ptr_eq(self_value, other_value))
})
}
}
impl<In: 'static, Out: 'static> Callable<In, Out> for UnsyncCallback<In, Out> {
fn try_run(&self, input: In) -> Option<Out> {
self.0.try_with_value(|fun| fun(input))
}
fn run(&self, input: In) -> Out {
self.0.with_value(|fun| fun(input))
}
@@ -185,12 +158,10 @@ impl<In, Out> fmt::Debug for Callback<In, Out> {
}
impl<In, Out> Callable<In, Out> for Callback<In, Out> {
fn try_run(&self, input: In) -> Option<Out> {
self.0.try_with_value(|fun| fun(input))
}
fn run(&self, input: In) -> Out {
self.0.with_value(|f| f(input))
self.0
.try_with_value(|f| f(input))
.expect("called a callback that has been disposed")
}
}
@@ -200,12 +171,6 @@ impl<In, Out> Clone for Callback<In, Out> {
}
}
impl<In, Out> Dispose for Callback<In, Out> {
fn dispose(self) {
self.0.dispose();
}
}
impl<In, Out> Copy for Callback<In, Out> {}
macro_rules! impl_callable_from_fn {
@@ -247,40 +212,25 @@ impl<In: 'static, Out: 'static> Callback<In, Out> {
{
Self(StoredValue::new(Arc::new(fun)))
}
/// Returns `true` if both callbacks wrap the same underlying function pointer.
#[inline]
pub fn matches(&self, other: &Self) -> bool {
self.0
.try_with_value(|self_value| {
other.0.try_with_value(|other_value| {
Arc::ptr_eq(self_value, other_value)
})
})
.flatten()
.unwrap_or(false)
}
}
#[cfg(test)]
mod tests {
use super::Callable;
use crate::callback::{Callback, UnsyncCallback};
use reactive_graph::traits::Dispose;
struct NoClone {}
#[test]
fn clone_callback() {
let callback = Callback::new(move |_no_clone: NoClone| NoClone {});
let _cloned = callback;
let _cloned = callback.clone();
}
#[test]
fn clone_unsync_callback() {
let callback =
UnsyncCallback::new(move |_no_clone: NoClone| NoClone {});
let _cloned = callback;
let _cloned = callback.clone();
}
#[test]
@@ -296,48 +246,4 @@ mod tests {
let _callback: UnsyncCallback<(i32, String), String> =
(|num, s| format!("{num} {s}")).into();
}
#[test]
fn sync_callback_try_run() {
let callback = Callback::new(move |arg| arg);
assert_eq!(callback.try_run((0,)), Some((0,)));
callback.dispose();
assert_eq!(callback.try_run((0,)), None);
}
#[test]
fn unsync_callback_try_run() {
let callback = UnsyncCallback::new(move |arg| arg);
assert_eq!(callback.try_run((0,)), Some((0,)));
callback.dispose();
assert_eq!(callback.try_run((0,)), None);
}
#[test]
fn callback_matches_same() {
let callback1 = Callback::new(|x: i32| x * 2);
let callback2 = callback1.clone();
assert!(callback1.matches(&callback2));
}
#[test]
fn callback_matches_different() {
let callback1 = Callback::new(|x: i32| x * 2);
let callback2 = Callback::new(|x: i32| x + 1);
assert!(!callback1.matches(&callback2));
}
#[test]
fn unsync_callback_matches_same() {
let callback1 = UnsyncCallback::new(|x: i32| x * 2);
let callback2 = callback1.clone();
assert!(callback1.matches(&callback2));
}
#[test]
fn unsync_callback_matches_different() {
let callback1 = UnsyncCallback::new(|x: i32| x * 2);
let callback2 = UnsyncCallback::new(|x: i32| x + 1);
assert!(!callback1.matches(&callback2));
}
}

View File

@@ -85,7 +85,7 @@ type BoxedChildrenFn = Box<dyn Fn() -> AnyView + Send>;
/// )
/// }
pub trait ToChildren<F> {
/// Convert the provided type (generally a closure) to Self (generally a "children" type,
/// Convert the provided type to (generally a closure) to Self (generally a "children" type,
/// e.g., [Children]). See the implementations to see exactly which input types are supported
/// and which "children" type they are converted to.
fn to_children(f: F) -> Self;
@@ -228,7 +228,6 @@ impl ViewFnOnce {
pub struct TypedChildren<T>(Box<dyn FnOnce() -> View<T> + Send>);
impl<T> TypedChildren<T> {
/// Extracts the inner `children` function.
pub fn into_inner(self) -> impl FnOnce() -> View<T> + Send {
self.0
}
@@ -246,7 +245,7 @@ where
}
}
/// A typed equivalent to [`ChildrenFnMut`], which takes a generic but preserves type information to
/// A typed equivalent to [`ChildrenMut`], which takes a generic but preserves type information to
/// allow the compiler to optimize the view more effectively.
pub struct TypedChildrenMut<T>(Box<dyn FnMut() -> View<T> + Send>);
@@ -257,7 +256,6 @@ impl<T> Debug for TypedChildrenMut<T> {
}
impl<T> TypedChildrenMut<T> {
/// Extracts the inner `children` function.
pub fn into_inner(self) -> impl FnMut() -> View<T> + Send {
self.0
}
@@ -285,15 +283,7 @@ impl<T> Debug for TypedChildrenFn<T> {
}
}
impl<T> Clone for TypedChildrenFn<T> {
// Manual implementation to avoid the `T: Clone` bound.
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<T> TypedChildrenFn<T> {
/// Extracts the inner `children` function.
pub fn into_inner(self) -> Arc<dyn Fn() -> View<T> + Send + Sync> {
self.0
}

View File

@@ -11,13 +11,13 @@ use reactive_graph::{
use rustc_hash::FxHashMap;
use std::{fmt::Debug, sync::Arc};
use tachys::{
html::attribute::{any_attribute::AnyAttribute, Attribute},
html::attribute::Attribute,
hydration::Cursor,
reactive_graph::OwnedView,
ssr::StreamBuilder,
view::{
add_attr::AddAnyAttr, any_view::ExtraAttrsMut, Mountable, Position,
PositionState, Render, RenderHtml,
add_attr::AddAnyAttr, Mountable, Position, PositionState, Render,
RenderHtml,
},
};
use throw_error::{Error, ErrorHook, ErrorId};
@@ -173,10 +173,10 @@ where
{
type State = RenderEffect<ErrorBoundaryViewState<Chil::State, Fal::State>>;
fn build(mut self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
fn build(mut self) -> Self::State {
let hook = Arc::clone(&self.hook);
let _hook = throw_error::set_error_hook(Arc::clone(&hook));
let mut children = Some(self.children.build(extra_attrs.clone()));
let mut children = Some(self.children.build());
RenderEffect::new(
move |prev: Option<
ErrorBoundaryViewState<Chil::State, Fal::State>,
@@ -193,8 +193,7 @@ where
// yes errors, and was showing children
(false, None) => {
state.fallback = Some(
(self.fallback)(self.errors.clone())
.build(extra_attrs.clone()),
(self.fallback)(self.errors.clone()).build(),
);
state
.children
@@ -208,10 +207,8 @@ where
}
state
} else {
let fallback = (!self.errors_empty.get()).then(|| {
(self.fallback)(self.errors.clone())
.build(extra_attrs.clone())
});
let fallback = (!self.errors_empty.get())
.then(|| (self.fallback)(self.errors.clone()).build());
ErrorBoundaryViewState {
children: children.take().unwrap(),
fallback,
@@ -221,12 +218,8 @@ where
)
}
fn rebuild(
self,
state: &mut Self::State,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
let new = self.build(extra_attrs);
fn rebuild(self, state: &mut Self::State) {
let new = self.build();
let mut old = std::mem::replace(state, new);
old.insert_before_this(state);
old.unmount();
@@ -275,18 +268,14 @@ where
Fal: RenderHtml + Send + 'static,
{
type AsyncOutput = ErrorBoundaryView<Chil::AsyncOutput, FalFn>;
type Owned = Self;
const MIN_LENGTH: usize = Chil::MIN_LENGTH;
fn dry_resolve(&mut self, extra_attrs: ExtraAttrsMut<'_>) {
self.children.dry_resolve(extra_attrs);
fn dry_resolve(&mut self) {
self.children.dry_resolve();
}
async fn resolve(
self,
extra_attrs: ExtraAttrsMut<'_>,
) -> Self::AsyncOutput {
async fn resolve(self) -> Self::AsyncOutput {
let ErrorBoundaryView {
hook,
boundary_id,
@@ -300,7 +289,7 @@ where
hook,
boundary_id,
errors_empty,
children: children.resolve(extra_attrs).await,
children: children.resolve().await,
fallback,
errors,
}
@@ -312,7 +301,6 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
// first, attempt to serialize the children to HTML, then check for errors
let _hook = throw_error::set_error_hook(self.hook);
@@ -323,7 +311,6 @@ where
&mut new_pos,
escape,
mark_branches,
extra_attrs.clone(),
);
// any thrown errors would've been caught here
@@ -336,7 +323,6 @@ where
position,
escape,
mark_branches,
extra_attrs,
);
}
}
@@ -347,7 +333,6 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) where
Self: Sized,
{
@@ -360,7 +345,6 @@ where
&mut new_pos,
escape,
mark_branches,
extra_attrs.clone(),
);
// any thrown errors would've been caught here
@@ -374,7 +358,6 @@ where
position,
escape,
mark_branches,
extra_attrs,
);
buf.push_sync(&fallback);
}
@@ -384,7 +367,6 @@ where
mut self,
cursor: &Cursor,
position: &PositionState,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
let mut children = Some(self.children);
let hook = Arc::clone(&self.hook);
@@ -406,8 +388,7 @@ where
// yes errors, and was showing children
(false, None) => {
state.fallback = Some(
(self.fallback)(self.errors.clone())
.build(extra_attrs.clone()),
(self.fallback)(self.errors.clone()).build(),
);
state
.children
@@ -424,23 +405,15 @@ where
let children = children.take().unwrap();
let (children, fallback) = if self.errors_empty.get() {
(
children.hydrate::<FROM_SERVER>(
&cursor,
&position,
extra_attrs.clone(),
),
children.hydrate::<FROM_SERVER>(&cursor, &position),
None,
)
} else {
(
children.build(extra_attrs.clone()),
children.build(),
Some(
(self.fallback)(self.errors.clone())
.hydrate::<FROM_SERVER>(
&cursor,
&position,
extra_attrs.clone(),
),
.hydrate::<FROM_SERVER>(&cursor, &position),
),
)
};
@@ -450,10 +423,6 @@ where
},
)
}
fn into_owned(self) -> Self::Owned {
self
}
}
#[derive(Debug)]

View File

@@ -44,67 +44,6 @@ use tachys::{reactive_graph::OwnedView, view::keyed::keyed};
/// }
/// }
/// ```
///
/// For convenience, you can also choose to write template code directly in the `<For>`
/// component, using the `let` syntax:
///
/// ```
/// # use leptos::prelude::*;
///
/// # #[derive(Copy, Clone, Debug, PartialEq, Eq)]
/// # struct Counter {
/// # id: usize,
/// # count: RwSignal<i32>
/// # }
/// #
/// # #[component]
/// # fn Counters() -> impl IntoView {
/// # let (counters, set_counters) = create_signal::<Vec<Counter>>(vec![]);
/// #
/// view! {
/// <div>
/// <For
/// each=move || counters.get()
/// key=|counter| counter.id
/// let(counter)
/// >
/// <button>"Value: " {move || counter.count.get()}</button>
/// </For>
/// </div>
/// }
/// # }
/// ```
///
/// The `let` syntax also supports destructuring the pattern of your data.
/// `let((one, two))` in the case of tuples, and `let(Struct { field_one, field_two })`
/// in the case of structs.
///
/// ```
/// # use leptos::prelude::*;
///
/// # #[derive(Copy, Clone, Debug, PartialEq, Eq)]
/// # struct Counter {
/// # id: usize,
/// # count: RwSignal<i32>
/// # }
/// #
/// # #[component]
/// # fn Counters() -> impl IntoView {
/// # let (counters, set_counters) = create_signal::<Vec<Counter>>(vec![]);
/// #
/// view! {
/// <div>
/// <For
/// each=move || counters.get()
/// key=|counter| counter.id
/// let(Counter { id, count })
/// >
/// <button>"Value: " {move || count.get()}</button>
/// </For>
/// </div>
/// }
/// # }
/// ```
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
#[component]
pub fn For<IF, I, T, EF, N, KF, K>(

View File

@@ -3,11 +3,7 @@ use leptos_dom::helpers::window;
use leptos_server::{ServerAction, ServerMultiAction};
use serde::de::DeserializeOwned;
use server_fn::{
client::Client,
codec::PostUrl,
error::{IntoAppError, ServerFnErrorErr},
request::ClientReq,
ServerFn,
client::Client, codec::PostUrl, request::ClientReq, ServerFn, ServerFnError,
};
use tachys::{
either::Either,
@@ -76,7 +72,9 @@ use web_sys::{
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
#[component]
pub fn ActionForm<ServFn>(
/// The action from which to build the form.
/// The action from which to build the form. This should include a URL, which can be generated
/// by default using [`create_server_action`](leptos_server::create_server_action) or added
/// manually using [`using_server_fn`](leptos_server::Action::using_server_fn).
action: ServerAction<ServFn>,
/// A [`NodeRef`] in which the `<form>` element should be stored.
#[prop(optional)]
@@ -125,10 +123,9 @@ where
"Error converting form field into server function \
arguments: {err:?}"
);
value.set(Some(Err(ServerFnErrorErr::Serialization(
value.set(Some(Err(ServerFnError::Serialization(
err.to_string(),
)
.into_app_error())));
))));
version.update(|n| *n += 1);
}
}
@@ -152,7 +149,9 @@ where
/// progressively enhanced to use client-side routing.
#[component]
pub fn MultiActionForm<ServFn>(
/// The action from which to build the form.
/// The action from which to build the form. This should include a URL, which can be generated
/// by default using [create_server_action](leptos_server::create_server_action) or added
/// manually using [leptos_server::Action::using_server_fn].
action: ServerMultiAction<ServFn>,
/// A [`NodeRef`] in which the `<form>` element should be stored.
#[prop(optional)]
@@ -192,10 +191,9 @@ where
action.dispatch(new_input);
}
Err(err) => {
action.dispatch_sync(Err(ServerFnErrorErr::Serialization(
action.dispatch_sync(Err(ServerFnError::Serialization(
err.to_string(),
)
.into_app_error()));
)));
}
}
};
@@ -253,16 +251,12 @@ where
) -> Result<Self, serde_qs::Error>;
}
/// Errors that can arise when coverting from an HTML event or form into a Rust data type.
#[derive(Error, Debug)]
pub enum FromFormDataError {
/// Could not find a `<form>` connected to the event.
#[error("Could not find <form> connected to event.")]
MissingForm(Event),
/// Could not create `FormData` from the form.
#[error("Could not create FormData from <form>: {0:?}")]
FormData(JsValue),
/// Failed to deserialize this Rust type from the form data.
#[error("Deserialization error: {0:?}")]
Deserialization(serde_qs::Error),
}

View File

@@ -1,7 +1,7 @@
(function (root, pkg_path, output_name, wasm_output_name) {
import(`${root}/${pkg_path}/${output_name}.js`)
.then(mod => {
mod.default({module_or_path: `${root}/${pkg_path}/${wasm_output_name}.wasm`}).then(() => {
mod.default(`${root}/${pkg_path}/${wasm_output_name}.wasm`).then(() => {
mod.hydrate();
});
})

View File

@@ -1,6 +1,4 @@
((root, pkg_path, output_name, wasm_output_name) => {
let MOST_RECENT_CHILDREN_CB;
function idle(c) {
if ("requestIdleCallback" in window) {
window.requestIdleCallback(c);
@@ -8,49 +6,55 @@
c();
}
}
function hydrateIslands(rootNode, mod) {
function traverse(node) {
function islandTree(rootNode) {
const tree = [];
function traverse(node, parent) {
if (node.nodeType === Node.ELEMENT_NODE) {
const tag = node.tagName.toLowerCase();
if(tag === 'leptos-island') {
if(node.tagName.toLowerCase() === 'leptos-island') {
const children = [];
const id = node.dataset.component || null;
hydrateIsland(node, id, mod);
const data = { id, node, children };
for(const child of node.children) {
traverse(child, children);
}
(parent || tree).push(data);
} else {
if(tag === 'leptos-children') {
MOST_RECENT_CHILDREN_CB = node.$$on_hydrate;
}
for(const child of node.children) {
traverse(child);
traverse(child, parent);
};
}
}
}
traverse(rootNode);
traverse(rootNode, null);
return { el: null, id: null, children: tree };
}
function hydrateIsland(el, id, mod) {
const islandFn = mod[id];
if (islandFn) {
if (MOST_RECENT_CHILDREN_CB) {
MOST_RECENT_CHILDREN_CB();
}
islandFn(el);
} else {
console.warn(`Could not find WASM function for the island ${id}.`);
}
}
function hydrateIslands(entry, mod) {
if(entry.node) {
hydrateIsland(entry.node, entry.id, mod);
}
for (const island of entry.children) {
hydrateIslands(island, mod);
}
}
idle(() => {
import(`${root}/${pkg_path}/${output_name}.js`)
.then(mod => {
mod.default(`${root}/${pkg_path}/${wasm_output_name}.wasm`).then(() => {
mod.hydrate();
hydrateIslands(document.body, mod);
hydrateIslands(islandTree(document.body, null), mod);
});
})
});

View File

@@ -4,15 +4,9 @@ use crate::prelude::*;
use leptos_config::LeptosOptions;
use leptos_macro::{component, view};
/// Inserts auto-reloading code used in `cargo-leptos`.
///
/// This should be included in the `<head>` of your application shell during development.
#[component]
pub fn AutoReload(
/// Whether the file-watching feature should be disabled.
#[prop(optional)]
disable_watch: bool,
/// Configuration options for this project.
#[prop(optional)] disable_watch: bool,
options: LeptosOptions,
) -> impl IntoView {
(!disable_watch && std::env::var("LEPTOS_WATCH").is_ok()).then(|| {
@@ -40,16 +34,10 @@ pub fn AutoReload(
})
}
/// Inserts hydration scripts that add interactivity to your server-rendered HTML.
///
/// This should be included in the `<head>` of your application shell.
#[component]
pub fn HydrationScripts(
/// Configuration options for this project.
options: LeptosOptions,
/// Should be `true` to hydrate in `islands` mode.
#[prop(optional)]
islands: bool,
#[prop(optional)] islands: bool,
/// A base url, not including a trailing slash
#[prop(optional, into)]
root: Option<String>,

View File

@@ -1,15 +1,14 @@
use std::borrow::Cow;
use tachys::{
html::attribute::{any_attribute::AnyAttribute, Attribute},
html::attribute::Attribute,
hydration::Cursor,
ssr::StreamBuilder,
view::{
add_attr::AddAnyAttr, any_view::ExtraAttrsMut, Position, PositionState,
Render, RenderHtml, ToTemplate,
add_attr::AddAnyAttr, Position, PositionState, Render, RenderHtml,
ToTemplate,
},
};
/// A wrapper for any kind of view.
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct View<T>
where
@@ -21,7 +20,6 @@ where
}
impl<T> View<T> {
/// Wraps the view.
pub fn new(inner: T) -> Self {
Self {
inner,
@@ -30,12 +28,10 @@ impl<T> View<T> {
}
}
/// Unwraps the view, returning the inner type.
pub fn into_inner(self) -> T {
self.inner
}
/// Adds a view marker, which is used for hot-reloading and debug purposes.
#[inline(always)]
pub fn with_view_marker(
#[allow(unused_mut)] // used in debug
@@ -51,12 +47,10 @@ impl<T> View<T> {
}
}
/// A trait that is implemented for types that can be rendered.
pub trait IntoView
where
Self: Sized + Render + RenderHtml + Send,
{
/// Wraps the inner type.
fn into_view(self) -> View<Self>;
}
@@ -76,34 +70,26 @@ where
impl<T: Render> Render for View<T> {
type State = T::State;
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
self.inner.build(extra_attrs)
fn build(self) -> Self::State {
self.inner.build()
}
fn rebuild(
self,
state: &mut Self::State,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
self.inner.rebuild(state, extra_attrs)
fn rebuild(self, state: &mut Self::State) {
self.inner.rebuild(state)
}
}
impl<T: RenderHtml> RenderHtml for View<T> {
type AsyncOutput = T::AsyncOutput;
type Owned = View<T::Owned>;
const MIN_LENGTH: usize = <T as RenderHtml>::MIN_LENGTH;
async fn resolve(
self,
extra_attrs: ExtraAttrsMut<'_>,
) -> Self::AsyncOutput {
self.inner.resolve(extra_attrs).await
async fn resolve(self) -> Self::AsyncOutput {
self.inner.resolve().await
}
fn dry_resolve(&mut self, extra_attrs: ExtraAttrsMut<'_>) {
self.inner.dry_resolve(extra_attrs);
fn dry_resolve(&mut self) {
self.inner.dry_resolve();
}
fn to_html_with_buf(
@@ -112,7 +98,6 @@ impl<T: RenderHtml> RenderHtml for View<T> {
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
#[cfg(debug_assertions)]
let vm = self.view_marker.to_owned();
@@ -121,13 +106,8 @@ impl<T: RenderHtml> RenderHtml for View<T> {
buf.push_str(&format!("<!--hot-reload|{vm}|open-->"));
}
self.inner.to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs,
);
self.inner
.to_html_with_buf(buf, position, escape, mark_branches);
#[cfg(debug_assertions)]
if let Some(vm) = vm.as_ref() {
@@ -141,7 +121,6 @@ impl<T: RenderHtml> RenderHtml for View<T> {
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) where
Self: Sized,
{
@@ -157,7 +136,6 @@ impl<T: RenderHtml> RenderHtml for View<T> {
position,
escape,
mark_branches,
extra_attrs,
);
#[cfg(debug_assertions)]
@@ -170,18 +148,8 @@ impl<T: RenderHtml> RenderHtml for View<T> {
self,
cursor: &Cursor,
position: &PositionState,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
self.inner
.hydrate::<FROM_SERVER>(cursor, position, extra_attrs)
}
fn into_owned(self) -> Self::Owned {
View {
inner: self.inner.into_owned(),
#[cfg(debug_assertions)]
view_marker: self.view_marker,
}
self.inner.hydrate::<FROM_SERVER>(cursor, position)
}
}
@@ -220,15 +188,9 @@ impl<T: AddAnyAttr> AddAnyAttr for View<T> {
}
}
/// Collects some iterator of views into a list, so they can be rendered.
///
/// This is a shorthand for `.collect::<Vec<_>>()`, and allows any iterator of renderable
/// items to be collected into a renderable collection.
pub trait CollectView {
/// The inner view type.
type View: IntoView;
/// Collects the iterator into a list of views.
fn collect_view(self) -> Vec<Self::View>;
}

View File

@@ -1,6 +1,5 @@
#![deny(missing_docs)]
#!rdeny(missing_docs)]
#![forbid(unsafe_code)]
//! # About Leptos
//!
//! Leptos is a full-stack framework for building web applications in Rust. You can use it to build
@@ -39,7 +38,7 @@
//! 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 [`Effect::new`](leptos::prelude::Effect) to
//! 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).
@@ -78,7 +77,7 @@
//! + `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`] trait.
//! - **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`](macro@leptos::prelude::server) macro and [`ServerAction`](leptos::prelude::ServerAction).
//!
@@ -172,7 +171,7 @@ pub mod prelude {
actions::*, computed::*, effect::*, graph::untrack, owner::*,
signal::*, wrappers::read::*,
};
pub use server_fn::{self, error::ServerFnError};
pub use server_fn::{self, ServerFnError};
pub use tachys::{
reactive_graph::{bind::BindAttribute, node_ref::*, Suspend},
view::{
@@ -192,9 +191,6 @@ pub mod callback;
/// Types that can be passed as the `children` prop of a component.
pub mod children;
/// Wrapper for intercepting component attributes.
pub mod attribute_interceptor;
#[doc(hidden)]
/// Traits used to implement component constructors.
pub mod component;
@@ -291,9 +287,8 @@ pub mod logging {
pub use leptos_dom::{debug_warn, error, log, warn};
}
/// Utilities for working with asynchronous tasks.
pub mod task {
pub use any_spawner::{self, CustomExecutor, Executor};
pub use any_spawner::Executor;
use std::future::Future;
/// Spawns a thread-safe [`Future`].
@@ -321,10 +316,10 @@ pub mod task {
}
// these reexports are used in islands
#[cfg(feature = "islands")]
#[cfg(feature = "experimental-islands")]
#[doc(hidden)]
pub use serde;
#[cfg(feature = "islands")]
#[cfg(feature = "experimental-islands")]
#[doc(hidden)]
pub use serde_json;
#[cfg(feature = "tracing")]

View File

@@ -71,7 +71,6 @@ where
view.hydrate::<true>(
&Cursor::new(parent.unchecked_into()),
&PositionState::default(),
None,
)
});
@@ -125,7 +124,7 @@ where
let owner = Owner::new();
let mountable = owner.with(move || {
let view = f().into_view();
let mut mountable = view.build(None);
let mut mountable = view.build();
mountable.mount(&parent, None);
mountable
});
@@ -153,7 +152,7 @@ where
let owner = Owner::new();
let mountable = owner.with(move || {
let view = f();
let mut mountable = view.build(None);
let mut mountable = view.build();
mountable.mount(parent, None);
mountable
});

View File

@@ -51,13 +51,6 @@ use tachys::html::attribute::AttributeValue;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Nonce(pub(crate) Arc<str>);
impl Nonce {
/// Returns a reference to the inner reference-counted string slice representing the nonce.
pub fn as_inner(&self) -> &Arc<str> {
&self.0
}
}
impl Deref for Nonce {
type Target = str;

View File

@@ -16,16 +16,14 @@ use reactive_graph::{
traits::{Dispose, Get, Read, Track, With},
};
use slotmap::{DefaultKey, SlotMap};
use std::sync::Arc;
use tachys::{
either::Either,
html::attribute::{any_attribute::AnyAttribute, Attribute},
html::attribute::Attribute,
hydration::Cursor,
reactive_graph::{OwnedView, OwnedViewState},
ssr::StreamBuilder,
view::{
add_attr::AddAnyAttr,
any_view::ExtraAttrsMut,
either::{EitherKeepAlive, EitherKeepAliveState},
Mountable, Position, PositionState, Render, RenderHtml,
},
@@ -89,12 +87,7 @@ use throw_error::ErrorHookFuture;
/// ```
#[component]
pub fn Suspense<Chil>(
/// A function that returns a fallback that will be shown while resources are still loading.
/// By default this is an empty view.
#[prop(optional, into)]
fallback: ViewFnOnce,
/// Children will be rendered once initially to catch any resource reads, then hidden until all
/// data have loaded.
#[prop(optional, into)] fallback: ViewFnOnce,
children: TypedChildren<Chil>,
) -> impl IntoView
where
@@ -134,18 +127,6 @@ where
})
}
fn nonce_or_not() -> Option<Arc<str>> {
#[cfg(feature = "nonce")]
{
use crate::nonce::Nonce;
use_context::<Nonce>().map(|n| n.0)
}
#[cfg(not(feature = "nonce"))]
{
None
}
}
pub(crate) struct SuspenseBoundary<const TRANSITION: bool, Fal, Chil> {
pub id: SerializedDataId,
pub none_pending: ArcMemo<bool>,
@@ -163,7 +144,7 @@ where
OwnedViewState<EitherKeepAliveState<Chil::State, Fal::State>>,
>;
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
fn build(self) -> Self::State {
let mut children = Some(self.children);
let mut fallback = Some(self.fallback);
let none_pending = self.none_pending;
@@ -188,20 +169,16 @@ where
);
if let Some(mut state) = prev {
this.rebuild(&mut state, extra_attrs.clone());
this.rebuild(&mut state);
state
} else {
this.build(extra_attrs.clone())
this.build()
}
})
}
fn rebuild(
self,
state: &mut Self::State,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
let new = self.build(extra_attrs);
fn rebuild(self, state: &mut Self::State) {
let new = self.build();
let mut old = std::mem::replace(state, new);
old.insert_before_this(state);
old.unmount();
@@ -252,16 +229,12 @@ where
// i.e., if this is the child of another Suspense during SSR, don't wait for it: it will handle
// itself
type AsyncOutput = Self;
type Owned = Self;
const MIN_LENGTH: usize = Chil::MIN_LENGTH;
fn dry_resolve(&mut self, _extra_attrs: ExtraAttrsMut<'_>) {}
fn dry_resolve(&mut self) {}
async fn resolve(
self,
_extra_attrs: ExtraAttrsMut<'_>,
) -> Self::AsyncOutput {
async fn resolve(self) -> Self::AsyncOutput {
self
}
@@ -271,15 +244,9 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
self.fallback.to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs,
);
self.fallback
.to_html_with_buf(buf, position, escape, mark_branches);
}
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
@@ -288,7 +255,6 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
mut extra_attrs: Option<Vec<AnyAttribute>>,
) where
Self: Sized,
{
@@ -313,8 +279,7 @@ where
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(ExtraAttrsMut::from_owned(&mut extra_attrs));
self.children.dry_resolve();
// check the set of tasks to see if it is empty, now or later
let eff = reactive_graph::effect::Effect::new_isomorphic({
@@ -330,8 +295,7 @@ where
}
});
let mut fut = Box::pin(ScopedFuture::new(ErrorHookFuture::new({
let mut extra_attrs = extra_attrs.clone();
let mut fut = Box::pin(ScopedFuture::new(ErrorHookFuture::new(
async move {
// race the local resource notifier against the set of tasks
//
@@ -358,7 +322,7 @@ where
// but in situations like a <For each=|| some_resource.snapshot()/> we actually
// want to be able to 1) synchronously read a resource's value, but still 2) wait
// for it to load before we render everything
let mut children = Box::pin(self.children.resolve(ExtraAttrsMut::from_owned(&mut extra_attrs)).fuse());
let mut children = Box::pin(self.children.resolve().fuse());
// we continue racing the children against the "do we have any local
// resources?" Future
@@ -377,8 +341,8 @@ where
}
}
}
}
})));
},
)));
match fut.as_mut().now_or_never() {
Some(Some(resolved)) => {
Either::<Fal, _>::Right(resolved)
@@ -387,7 +351,6 @@ where
position,
escape,
mark_branches,
extra_attrs,
);
}
Some(None) => {
@@ -397,7 +360,6 @@ where
position,
escape,
mark_branches,
extra_attrs,
);
}
None => {
@@ -411,15 +373,8 @@ where
self.fallback,
&mut fallback_position,
mark_branches,
extra_attrs.clone(),
);
buf.push_async_out_of_order_with_nonce(
fut,
position,
mark_branches,
nonce_or_not(),
extra_attrs,
);
buf.push_async_out_of_order(fut, position, mark_branches);
} else {
buf.push_async({
let mut position = *position;
@@ -434,7 +389,6 @@ where
&mut position,
escape,
mark_branches,
extra_attrs,
);
builder.finish().take_chunks()
}
@@ -449,7 +403,6 @@ where
self,
cursor: &Cursor,
position: &PositionState,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
let cursor = cursor.to_owned();
let position = position.to_owned();
@@ -478,21 +431,13 @@ where
);
if let Some(mut state) = prev {
this.rebuild(&mut state, extra_attrs.clone());
this.rebuild(&mut state);
state
} else {
this.hydrate::<FROM_SERVER>(
&cursor,
&position,
extra_attrs.clone(),
)
this.hydrate::<FROM_SERVER>(&cursor, &position)
}
})
}
fn into_owned(self) -> Self::Owned {
self
}
}
/// A wrapper that prevents [`Suspense`] from waiting for any resource reads that happen inside
@@ -512,16 +457,12 @@ where
{
type State = T::State;
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
(self.0)().build(extra_attrs)
fn build(self) -> Self::State {
(self.0)().build()
}
fn rebuild(
self,
state: &mut Self::State,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
(self.0)().rebuild(state, extra_attrs);
fn rebuild(self, state: &mut Self::State) {
(self.0)().rebuild(state);
}
}
@@ -549,16 +490,12 @@ where
T: RenderHtml + 'static,
{
type AsyncOutput = Self;
type Owned = Self;
const MIN_LENGTH: usize = T::MIN_LENGTH;
fn dry_resolve(&mut self, _extra_attrs: ExtraAttrsMut<'_>) {}
fn dry_resolve(&mut self) {}
async fn resolve(
self,
_extra_attrs: ExtraAttrsMut<'_>,
) -> Self::AsyncOutput {
async fn resolve(self) -> Self::AsyncOutput {
self
}
@@ -568,15 +505,8 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
(self.0)().to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs,
);
(self.0)().to_html_with_buf(buf, position, escape, mark_branches);
}
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
@@ -585,7 +515,6 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) where
Self: Sized,
{
@@ -594,7 +523,6 @@ where
position,
escape,
mark_branches,
extra_attrs,
);
}
@@ -602,12 +530,7 @@ where
self,
cursor: &Cursor,
position: &PositionState,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
(self.0)().hydrate::<FROM_SERVER>(cursor, position, extra_attrs)
}
fn into_owned(self) -> Self::Owned {
self
(self.0)().hydrate::<FROM_SERVER>(cursor, position)
}
}

View File

@@ -1,6 +1,5 @@
use oco_ref::Oco;
use std::sync::Arc;
use tachys::prelude::IntoAttributeValue;
/// Describes a value that is either a static or a reactive string, i.e.,
/// a [`String`], a [`&str`], or a reactive `Fn() -> String`.
@@ -74,11 +73,3 @@ impl Default for TextProp {
Self(Arc::new(|| Oco::Borrowed("")))
}
}
impl IntoAttributeValue for TextProp {
type Output = Oco<'static, str>;
fn into_attribute_value(self) -> Self::Output {
self.get()
}
}

View File

@@ -85,44 +85,41 @@ pub fn Transition<Chil>(
where
Chil: IntoView + Send + 'static,
{
let owner = Owner::new();
owner.with(|| {
let (starts_local, id) = {
Owner::current_shared_context()
.map(|sc| {
let id = sc.next_id();
(sc.get_incomplete_chunk(&id), id)
})
.unwrap_or_else(|| (false, Default::default()))
};
let fallback = fallback.run();
let children = children.into_inner()();
let tasks = ArcRwSignal::new(SlotMap::<DefaultKey, ()>::new());
provide_context(SuspenseContext {
tasks: tasks.clone(),
});
let none_pending = ArcMemo::new(move |prev: Option<&bool>| {
tasks.track();
if prev.is_none() && starts_local {
false
} else {
tasks.with(SlotMap::is_empty)
let (starts_local, id) = {
Owner::current_shared_context()
.map(|sc| {
let id = sc.next_id();
(sc.get_incomplete_chunk(&id), id)
})
.unwrap_or_else(|| (false, Default::default()))
};
let fallback = fallback.run();
let children = children.into_inner()();
let tasks = ArcRwSignal::new(SlotMap::<DefaultKey, ()>::new());
provide_context(SuspenseContext {
tasks: tasks.clone(),
});
let none_pending = ArcMemo::new(move |prev: Option<&bool>| {
tasks.track();
if prev.is_none() && starts_local {
false
} else {
tasks.with(SlotMap::is_empty)
}
});
if let Some(set_pending) = set_pending {
Effect::new_isomorphic({
let none_pending = none_pending.clone();
move |_| {
set_pending.set(!none_pending.get());
}
});
if let Some(set_pending) = set_pending {
Effect::new_isomorphic({
let none_pending = none_pending.clone();
move |_| {
set_pending.set(!none_pending.get());
}
});
}
}
OwnedView::new(SuspenseBoundary::<true, _, _> {
id,
none_pending,
fallback,
children,
})
OwnedView::new(SuspenseBoundary::<true, _, _> {
id,
none_pending,
fallback,
children,
})
}

File diff suppressed because it is too large Load Diff

View File

@@ -21,11 +21,8 @@ typed-builder = "0.20.0"
[dev-dependencies]
tokio = { version = "1.41", features = ["rt", "macros"] }
tempfile = "3.14"
tempfile = "3.13"
temp-env = { version = "0.3.6", features = ["async_closure"] }
[package.metadata.docs.rs]
rustdoc-args = ["--generate-link-to-definition"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }

View File

@@ -12,9 +12,8 @@ use typed_builder::TypedBuilder;
/// A Struct to allow us to parse LeptosOptions from the file. Not really needed, most interactions should
/// occur with LeptosOptions
#[derive(Clone, Debug, serde::Deserialize)]
#[derive(Clone, Debug, serde::Deserialize, Default)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub struct ConfFile {
pub leptos_options: LeptosOptions,
}
@@ -25,14 +24,9 @@ pub struct ConfFile {
/// It shares keys with cargo-leptos, to allow for easy interoperability
#[derive(TypedBuilder, Debug, Clone, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub struct LeptosOptions {
/// The name of the WASM and JS files generated by wasm-bindgen.
///
/// This should match the name that will be output when building your application.
///
/// You can easily set this using `env!("CARGO_CRATE_NAME")`.
#[builder(setter(into))]
/// The name of the WASM and JS files generated by wasm-bindgen. Defaults to the crate name with underscores instead of dashes
#[builder(setter(into), default=default_output_name())]
pub output_name: Arc<str>,
/// The path of the all the files generated by cargo-leptos. This defaults to '.' for convenience when integrating with other
/// tools.
@@ -84,40 +78,6 @@ pub struct LeptosOptions {
#[builder(default = default_hash_files())]
#[serde(default = "default_hash_files")]
pub hash_files: bool,
/// The default prefix to use for server functions when generating API routes. Can be
/// overridden for individual functions using `#[server(prefix = "...")]` as usual.
///
/// This is useful to override the default prefix (`/api`) for all server functions without
/// needing to manually specify via `#[server(prefix = "...")]` on every server function.
#[builder(default, setter(strip_option))]
#[serde(default)]
pub server_fn_prefix: Option<String>,
/// Whether to disable appending the server functions' hashes to the end of their API names.
///
/// This is useful when an app's client side needs a stable server API. For example, shipping
/// the CSR WASM binary in a Tauri app. Tauri app releases are dependent on each platform's
/// distribution method (e.g., the Apple App Store or the Google Play Store), which typically
/// are much slower than the frequency at which a website can be updated. In addition, it's
/// common for users to not have the latest app version installed. In these cases, the CSR WASM
/// app would need to be able to continue calling the backend server function API, so the API
/// path needs to be consistent and not have a hash appended.
///
/// Note that the hash suffixes is intended as a way to ensure duplicate API routes are created.
/// Without the hash, server functions will need to have unique names to avoid creating
/// duplicate routes. Axum will throw an error if a duplicate route is added to the router, but
/// Actix will not.
#[builder(default)]
#[serde(default)]
pub disable_server_fn_hash: bool,
/// Include the module path of the server function in the API route. This is an alternative
/// strategy to prevent duplicate server function API routes (the default strategy is to add
/// a hash to the end of the route). Each element of the module path will be separated by a `/`.
/// For example, a server function with a fully qualified name of `parent::child::server_fn`
/// would have an API route of `/api/parent/child/server_fn` (possibly with a
/// different prefix and a hash suffix depending on the values of the other server fn configs).
#[builder(default)]
#[serde(default)]
pub server_fn_mod_path: bool,
}
impl LeptosOptions {
@@ -160,14 +120,20 @@ impl LeptosOptions {
hash_file: env_w_default("LEPTOS_HASH_FILE_NAME", "hash.txt")?
.into(),
hash_files: env_w_default("LEPTOS_HASH_FILES", "false")?.parse()?,
server_fn_prefix: env_wo_default("SERVER_FN_PREFIX")?,
disable_server_fn_hash: env_wo_default("DISABLE_SERVER_FN_HASH")?
.is_some(),
server_fn_mod_path: env_wo_default("SERVER_FN_MOD_PATH")?.is_some(),
})
}
}
impl Default for LeptosOptions {
fn default() -> Self {
LeptosOptions::builder().build()
}
}
fn default_output_name() -> Arc<str> {
env!("CARGO_CRATE_NAME").replace('-', "_").into()
}
fn default_site_root() -> Arc<str> {
".".into()
}

View File

@@ -30,14 +30,14 @@ fn ws_from_str_test() {
#[test]
fn env_w_default_test() {
temp_env::with_var("LEPTOS_CONFIG_ENV_TEST", Some("custom"), || {
_ = temp_env::with_var("LEPTOS_CONFIG_ENV_TEST", Some("custom"), || {
assert_eq!(
env_w_default("LEPTOS_CONFIG_ENV_TEST", "default").unwrap(),
String::from("custom")
);
});
temp_env::with_var_unset("LEPTOS_CONFIG_ENV_TEST", || {
_ = temp_env::with_var_unset("LEPTOS_CONFIG_ENV_TEST", || {
assert_eq!(
env_w_default("LEPTOS_CONFIG_ENV_TEST", "default").unwrap(),
String::from("default")
@@ -47,14 +47,14 @@ fn env_w_default_test() {
#[test]
fn env_wo_default_test() {
temp_env::with_var("LEPTOS_CONFIG_ENV_TEST", Some("custom"), || {
_ = temp_env::with_var("LEPTOS_CONFIG_ENV_TEST", Some("custom"), || {
assert_eq!(
env_wo_default("LEPTOS_CONFIG_ENV_TEST").unwrap(),
Some(String::from("custom"))
);
});
temp_env::with_var_unset("LEPTOS_CONFIG_ENV_TEST", || {
_ = temp_env::with_var_unset("LEPTOS_CONFIG_ENV_TEST", || {
assert_eq!(env_wo_default("LEPTOS_CONFIG_ENV_TEST").unwrap(), None);
});
}

View File

@@ -12,10 +12,10 @@ edition.workspace = true
tachys = { workspace = true }
reactive_graph = { workspace = true }
or_poisoned = { workspace = true }
js-sys = "0.3.74"
js-sys = "0.3.72"
send_wrapper = "0.6.0"
tracing = { version = "0.1.41", optional = true }
wasm-bindgen = "0.2.97"
tracing = { version = "0.1.40", optional = true }
wasm-bindgen = "0.2.95"
serde_json = { version = "1.0", optional = true }
serde = { version = "1.0", optional = true }
@@ -37,6 +37,3 @@ rustdoc-args = ["--generate-link-to-definition"]
[package.metadata.cargo-all-features]
denylist = ["tracing"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }

View File

@@ -131,10 +131,6 @@ impl AnimationFrameRequestHandle {
/// Runs the given function between the next repaint using
/// [`Window.requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame).
///
/// ### Note about Context
///
/// The callback is called outside of the reactive ownership tree. This means that it does not have access to context via [`use_context`](reactive_graph::owner::use_context). If you want to use context inside the callback, you should either call `use_context` in the body of the component, and move the value into the callback, or access the current owner inside the component body using [`Owner::current`](reactive_graph::owner::Owner::current) and reestablish it in the callback with [`Owner::with`](reactive_graph::owner::Owner::with).
#[cfg_attr(feature = "tracing", instrument(level = "trace", skip_all))]
#[inline(always)]
pub fn request_animation_frame(cb: impl FnOnce() + 'static) {
@@ -163,10 +159,6 @@ fn closure_once(cb: impl FnOnce() + 'static) -> JsValue {
/// Runs the given function between the next repaint using
/// [`Window.requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame),
/// returning a cancelable handle.
///
/// ### Note about Context
///
/// The callback is called outside of the reactive ownership tree. This means that it does not have access to context via [`use_context`](reactive_graph::owner::use_context). If you want to use context inside the callback, you should either call `use_context` in the body of the component, and move the value into the callback, or access the current owner inside the component body using [`Owner::current`](reactive_graph::owner::Owner::current) and reestablish it in the callback with [`Owner::with`](reactive_graph::owner::Owner::with).
#[cfg_attr(feature = "tracing", instrument(level = "trace", skip_all))]
#[inline(always)]
pub fn request_animation_frame_with_handle(
@@ -205,10 +197,6 @@ impl IdleCallbackHandle {
/// Queues the given function during an idle period using
/// [`Window.requestIdleCallback`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestIdleCallback).
///
/// ### Note about Context
///
/// The callback is called outside of the reactive ownership tree. This means that it does not have access to context via [`use_context`](reactive_graph::owner::use_context). If you want to use context inside the callback, you should either call `use_context` in the body of the component, and move the value into the callback, or access the current owner inside the component body using [`Owner::current`](reactive_graph::owner::Owner::current) and reestablish it in the callback with [`Owner::with`](reactive_graph::owner::Owner::with).
#[cfg_attr(feature = "tracing", instrument(level = "trace", skip_all))]
#[inline(always)]
pub fn request_idle_callback(cb: impl Fn() + 'static) {
@@ -218,10 +206,6 @@ pub fn request_idle_callback(cb: impl Fn() + 'static) {
/// Queues the given function during an idle period using
/// [`Window.requestIdleCallback`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestIdleCallback),
/// returning a cancelable handle.
///
/// ### Note about Context
///
/// The callback is called outside of the reactive ownership tree. This means that it does not have access to context via [`use_context`](reactive_graph::owner::use_context). If you want to use context inside the callback, you should either call `use_context` in the body of the component, and move the value into the callback, or access the current owner inside the component body using [`Owner::current`](reactive_graph::owner::Owner::current) and reestablish it in the callback with [`Owner::with`](reactive_graph::owner::Owner::with).
#[cfg_attr(feature = "tracing", instrument(level = "trace", skip_all))]
#[inline(always)]
pub fn request_idle_callback_with_handle(
@@ -255,8 +239,6 @@ pub fn request_idle_callback_with_handle(
/// to perform final cleanup or other just-before-rendering tasks.
///
/// [MDN queueMicrotask](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask)
///
/// <div class="warning">The task is called outside of the ownership tree, this means that if you want to access for example the context you need to reestablish the owner.</div>
pub fn queue_microtask(task: impl FnOnce() + 'static) {
use js_sys::{Function, Reflect};
@@ -283,10 +265,6 @@ impl TimeoutHandle {
/// Executes the given function after the given duration of time has passed.
/// [`setTimeout()`](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout).
///
/// ### Note about Context
///
/// The callback is called outside of the reactive ownership tree. This means that it does not have access to context via [`use_context`](reactive_graph::owner::use_context). If you want to use context inside the callback, you should either call `use_context` in the body of the component, and move the value into the callback, or access the current owner inside the component body using [`Owner::current`](reactive_graph::owner::Owner::current) and reestablish it in the callback with [`Owner::with`](reactive_graph::owner::Owner::with).
#[cfg_attr(
feature = "tracing",
instrument(level = "trace", skip_all, fields(duration = ?duration))
@@ -297,10 +275,6 @@ pub fn set_timeout(cb: impl FnOnce() + 'static, duration: Duration) {
/// Executes the given function after the given duration of time has passed, returning a cancelable handle.
/// [`setTimeout()`](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout).
///
/// ### Note about Context
///
/// The callback is called outside of the reactive ownership tree. This means that it does not have access to context via [`use_context`](reactive_graph::owner::use_context). If you want to use context inside the callback, you should either call `use_context` in the body of the component, and move the value into the callback, or access the current owner inside the component body using [`Owner::current`](reactive_graph::owner::Owner::current) and reestablish it in the callback with [`Owner::with`](reactive_graph::owner::Owner::with).
#[cfg_attr(
feature = "tracing",
instrument(level = "trace", skip_all, fields(duration = ?duration))
@@ -357,10 +331,6 @@ pub fn set_timeout_with_handle(
/// }
/// }
/// ```
///
/// ### Note about Context
///
/// The callback is called outside of the reactive ownership tree. This means that it does not have access to context via [`use_context`](reactive_graph::owner::use_context). If you want to use context inside the callback, you should either call `use_context` in the body of the component, and move the value into the callback, or access the current owner inside the component body using [`Owner::current`](reactive_graph::owner::Owner::current) and reestablish it in the callback with [`Owner::with`](reactive_graph::owner::Owner::with).
pub fn debounce<T: 'static>(
delay: Duration,
mut cb: impl FnMut(T) + 'static,
@@ -428,10 +398,6 @@ impl IntervalHandle {
/// Repeatedly calls the given function, with a delay of the given duration between calls.
/// See [`setInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval).
///
/// ### Note about Context
///
/// The callback is called outside of the reactive ownership tree. This means that it does not have access to context via [`use_context`](reactive_graph::owner::use_context). If you want to use context inside the callback, you should either call `use_context` in the body of the component, and move the value into the callback, or access the current owner inside the component body using [`Owner::current`](reactive_graph::owner::Owner::current) and reestablish it in the callback with [`Owner::with`](reactive_graph::owner::Owner::with).
#[cfg_attr(
feature = "tracing",
instrument(level = "trace", skip_all, fields(duration = ?duration))
@@ -443,10 +409,6 @@ pub fn set_interval(cb: impl Fn() + 'static, duration: Duration) {
/// Repeatedly calls the given function, with a delay of the given duration between calls,
/// returning a cancelable handle.
/// See [`setInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval).
///
/// ### Note about Context
///
/// The callback is called outside of the reactive ownership tree. This means that it does not have access to context via [`use_context`](reactive_graph::owner::use_context). If you want to use context inside the callback, you should either call `use_context` in the body of the component, and move the value into the callback, or access the current owner inside the component body using [`Owner::current`](reactive_graph::owner::Owner::current) and reestablish it in the callback with [`Owner::with`](reactive_graph::owner::Owner::with).
#[cfg_attr(
feature = "tracing",
instrument(level = "trace", skip_all, fields(duration = ?duration))
@@ -489,10 +451,6 @@ pub fn set_interval_with_handle(
/// Adds an event listener to the `Window`, typed as a generic `Event`,
/// returning a cancelable handle.
///
/// ### Note about Context
///
/// The callback is called outside of the reactive ownership tree. This means that it does not have access to context via [`use_context`](reactive_graph::owner::use_context). If you want to use context inside the callback, you should either call `use_context` in the body of the component, and move the value into the callback, or access the current owner inside the component body using [`Owner::current`](reactive_graph::owner::Owner::current) and reestablish it in the callback with [`Owner::with`](reactive_graph::owner::Owner::with).
#[cfg_attr(
feature = "tracing",
instrument(level = "trace", skip_all, fields(event_name = %event_name))
@@ -561,10 +519,6 @@ pub fn window_event_listener_untyped(
/// on_cleanup(move || handle.remove());
/// }
/// ```
///
/// ### Note about Context
///
/// The callback is called outside of the reactive ownership tree. This means that it does not have access to context via [`use_context`](reactive_graph::owner::use_context). If you want to use context inside the callback, you should either call `use_context` in the body of the component, and move the value into the callback, or access the current owner inside the component body using [`Owner::current`](reactive_graph::owner::Owner::current) and reestablish it in the callback with [`Owner::with`](reactive_graph::owner::Owner::with).
pub fn window_event_listener<E: EventDescriptor + 'static>(
event: E,
cb: impl Fn(E::EventType) + 'static,

View File

@@ -29,9 +29,14 @@ macro_rules! error {
macro_rules! debug_warn {
($($x:tt)*) => {
{
if cfg!(debug_assertions) {
#[cfg(debug_assertions)]
{
$crate::warn!($($x)*)
}
#[cfg(not(debug_assertions))]
{
($($x)*)
}
}
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_macro"
version = "0.8.0-alpha"
version = "0.7.0-rc1"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
@@ -13,7 +13,7 @@ edition.workspace = true
proc-macro = true
[dependencies]
attribute-derive = { version = "0.10.3", features = ["syn-full"] }
attribute-derive = { version = "0.10.2", features = ["syn-full"] }
cfg-if = "1.0"
html-escape = "0.2.13"
itertools = "0.13.0"
@@ -27,14 +27,13 @@ leptos_hot_reload = { workspace = true }
server_fn_macro = { workspace = true }
convert_case = "0.6.0"
uuid = { version = "1.11", features = ["v4"] }
tracing = { version = "0.1.41", optional = true }
tracing = { version = "0.1.40", optional = true }
[dev-dependencies]
log = "0.4.22"
typed-builder = "0.20.0"
trybuild = "1.0"
leptos = { path = "../leptos" }
leptos_router = { path = "../router", features= ["ssr"] }
server_fn = { path = "../server_fn", features = ["cbor"] }
insta = "1.41"
serde = "1.0"
@@ -45,8 +44,7 @@ hydrate = []
ssr = ["server_fn_macro/ssr", "leptos/ssr"]
nightly = ["server_fn_macro/nightly"]
tracing = ["dep:tracing"]
islands = []
trace-components = []
experimental-islands = []
trace-component-props = []
actix = ["server_fn_macro/actix"]
axum = ["server_fn_macro/axum"]
@@ -85,7 +83,4 @@ skip_feature_sets = [
rustdoc-args = ["--generate-link-to-definition"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = [
'cfg(leptos_debuginfo)',
'cfg(erase_components)',
] }
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(erase_components)'] }

View File

@@ -201,35 +201,13 @@ impl ToTokens for Model {
) = {
#[cfg(feature = "tracing")]
{
/* TODO for 0.8: fix this
*
* The problem is that cargo now warns about an expected "tracing" cfg if
* you don't have a "tracing" feature in your actual crate
*
* However, until https://github.com/tokio-rs/tracing/pull/1819 is merged
* (?), you can't provide an alternate path for `tracing` (for example,
* ::leptos::tracing), which means that if you're going to use the macro
* you *must* have `tracing` in your Cargo.toml.
*
* Including the feature-check here causes cargo warnings on
* previously-working projects.
*
* Removing the feature-check here breaks any project that uses leptos with
* the tracing feature turned on, but without a tracing dependency in its
* Cargo.toml.
* /
*/
let instrument = cfg!(feature = "trace-components").then(|| quote! {
#[cfg_attr(
feature = "tracing",
::leptos::tracing::instrument(level = "info", name = #trace_name, skip_all)
)]
});
(
quote! {
#[allow(clippy::let_with_type_underscore)]
#instrument
#[cfg_attr(
feature = "tracing",
::leptos::tracing::instrument(level = "info", name = #trace_name, skip_all)
)]
},
quote! {
let __span = ::leptos::tracing::Span::current();
@@ -282,12 +260,8 @@ impl ToTokens for Model {
let body_name = unmodified_fn_name_from_fn_name(&body_name);
let body_expr = if is_island {
quote! {
::leptos::reactive::owner::Owner::new().with(|| {
::leptos::reactive::owner::Owner::with_hydration(move || {
::leptos::tachys::reactive_graph::OwnedView::new({
#body_name(#prop_names)
})
})
::leptos::reactive::owner::Owner::with_hydration(move || {
#body_name(#prop_names)
})
}
} else {
@@ -301,7 +275,7 @@ impl ToTokens for Model {
} else if cfg!(erase_components) {
quote! {
::leptos::prelude::IntoAny::into_any(
::leptos::reactive::graph::untrack_with_diagnostics(
::leptos::prelude::untrack(
move || {
#tracing_guard_expr
#tracing_props_expr
@@ -312,7 +286,7 @@ impl ToTokens for Model {
}
} else {
quote! {
::leptos::reactive::graph::untrack_with_diagnostics(
::leptos::prelude::untrack(
move || {
#tracing_guard_expr
#tracing_props_expr
@@ -327,8 +301,8 @@ impl ToTokens for Model {
let hydrate_fn_name = hydrate_fn_name.as_ref().unwrap();
quote! {
{
if ::leptos::context::use_context::<::leptos::reactive::owner::IsHydrating>()
.map(|h| h.0)
if ::leptos::reactive::owner::Owner::current_shared_context()
.map(|sc| sc.get_is_hydrating())
.unwrap_or(false) {
::leptos::either::Either::Left(
#component
@@ -365,23 +339,9 @@ impl ToTokens for Model {
let children = Box::new(|| {
let sc = ::leptos::reactive::owner::Owner::current_shared_context().unwrap();
let prev = sc.get_is_hydrating();
let owner = ::leptos::reactive::owner::Owner::new();
let value = owner.clone().with(|| {
::leptos::reactive::owner::Owner::with_no_hydration(move || {
::leptos::tachys::reactive_graph::OwnedView::new({
::leptos::tachys::html::islands::IslandChildren::new_with_on_hydrate(
children(),
{
let owner = owner.clone();
move || {
owner.set()
}
}
)
}).into_any()
})
});
let value = ::leptos::reactive::owner::Owner::with_no_hydration(||
::leptos::tachys::html::islands::IslandChildren::new(children()).into_any()
);
sc.set_is_hydrating(prev);
value
});
@@ -464,21 +424,20 @@ impl ToTokens for Model {
};
let children = if is_island_with_children {
quote! {
.children({
let owner = leptos::reactive::owner::Owner::current();
Box::new(move || {
.children({Box::new(|| {
use leptos::tachys::view::any_view::IntoAny;
::leptos::tachys::html::islands::IslandChildren::new_with_on_hydrate(
(),
{
let owner = owner.clone();
move || {
if let Some(owner) = &owner {
owner.set()
}
}
}
::leptos::tachys::html::islands::IslandChildren::new(
// TODO owner restoration for context
()
).into_any()})})
//.children(children)
/*.children(Box::new(|| {
use leptos::tachys::view::any_view::IntoAny;
::leptos::tachys::html::islands::IslandChildren::new(
// TODO owner restoration for context
()
).into_any()
}))*/
}
} else {
quote! {}
@@ -696,44 +655,14 @@ impl Prop {
abort!(e.span(), e.to_string());
});
let name = match *typed.pat {
Pat::Ident(i) => {
if let Some(name) = &prop_opts.name {
PatIdent {
attrs: vec![],
by_ref: None,
mutability: None,
ident: Ident::new(name, i.span()),
subpat: None,
}
} else {
i
}
}
Pat::Struct(_) | Pat::Tuple(_) | Pat::TupleStruct(_) => {
if let Some(name) = &prop_opts.name {
PatIdent {
attrs: vec![],
by_ref: None,
mutability: None,
ident: Ident::new(name, typed.pat.span()),
subpat: None,
}
} else {
abort!(
typed.pat,
"destructured props must be given a name e.g. \
#[prop(name = \"data\")]"
);
}
}
_ => {
abort!(
typed.pat,
"only `prop: bool` style types are allowed within the \
`#[component]` macro"
);
}
let name = if let Pat::Ident(i) = *typed.pat {
i
} else {
abort!(
typed.pat,
"only `prop: bool` style types are allowed within the \
`#[component]` macro"
);
};
Self {
@@ -936,7 +865,6 @@ struct PropOpt {
default: Option<syn::Expr>,
into: bool,
attrs: bool,
name: Option<String>,
}
struct TypedBuilderOpts {

View File

@@ -1,32 +0,0 @@
use convert_case::{Case, Casing};
use proc_macro::TokenStream;
use proc_macro2::Ident;
use proc_macro_error2::abort;
use quote::quote;
use syn::{spanned::Spanned, ItemFn};
pub fn lazy_impl(
_args: proc_macro::TokenStream,
s: TokenStream,
) -> TokenStream {
let fun = syn::parse::<ItemFn>(s).unwrap_or_else(|e| {
abort!(e.span(), "`lazy` can only be used on a function")
});
if fun.sig.asyncness.is_none() {
abort!(
fun.sig.asyncness.span(),
"`lazy` can only be used on an async function"
)
}
let converted_name = Ident::new(
&fun.sig.ident.to_string().to_case(Case::Snake),
fun.sig.ident.span(),
);
quote! {
#[cfg_attr(feature = "split", wasm_split::wasm_split(#converted_name))]
#fun
}
.into()
}

View File

@@ -1,5 +1,3 @@
//! Macros for use with the Leptos framework.
#![cfg_attr(feature = "nightly", feature(proc_macro_span))]
#![forbid(unsafe_code)]
// to prevent warnings from popping up when a nightly feature is stabilized
@@ -7,7 +5,6 @@
// FIXME? every use of quote! {} is warning here -- false positive?
#![allow(unknown_lints)]
#![allow(private_macro_use)]
#![deny(missing_docs)]
#[macro_use]
extern crate proc_macro_error2;
@@ -23,7 +20,6 @@ mod params;
mod view;
use crate::component::unmodified_fn_name_from_fn_name;
mod component;
mod lazy;
mod memo;
mod slice;
mod slot;
@@ -273,8 +269,8 @@ pub fn view(tokens: TokenStream) -> TokenStream {
view_macro_impl(tokens, false)
}
/// The `template` macro behaves like [`view`](view!), except that it wraps the entire tree in a
/// [`ViewTemplate`](https://docs.rs/leptos/0.7.0-gamma3/leptos/prelude/struct.ViewTemplate.html). This optimizes creation speed by rendering
/// The `template` macro behaves like [`view`], except that it wraps the entire tree in a
/// [`ViewTemplate`](leptos::prelude::ViewTemplate). This optimizes creation speed by rendering
/// most of the view into a `<template>` tag with HTML rendered at compile time, then hydrating it.
/// In exchange, there is a small binary size overhead.
#[proc_macro_error2::proc_macro_error]
@@ -367,7 +363,7 @@ fn normalized_call_site(site: proc_macro::Span) -> Option<String> {
}
}
/// This behaves like the [`view`](view!) macro, but loads the view from an external file instead of
/// This behaves like the [`view`] macro, but loads the view from an external file instead of
/// parsing it inline.
///
/// This is designed to allow editing views in a separate file, if this improves a user's workflow.
@@ -560,10 +556,10 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
}
/// Defines a component as an interactive island when you are using the
/// `islands` feature of Leptos. Apart from the macro name,
/// `experimental-islands` feature of Leptos. Apart from the macro name,
/// the API is the same as the [`component`](macro@component) macro.
///
/// When you activate the `islands` feature, every `#[component]`
/// When you activate the `experimental-islands` feature, every `#[component]`
/// is server-only by default. This "default to server" behavior is important:
/// you opt into shipping code to the client, rather than opting out. You can
/// opt into client-side interactivity for any given component by changing from
@@ -640,7 +636,7 @@ pub fn island(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
abort!(
transparent,
"only `transparent` is supported";
help = "try `#[island(transparent)]` or `#[island]`"
help = "try `#[component(transparent)]` or `#[component]`"
);
}
@@ -919,7 +915,7 @@ pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
args.into(),
s.into(),
Some(syn::parse_quote!(::leptos::server_fn)),
option_env!("SERVER_FN_PREFIX").unwrap_or("/api"),
"/api",
None,
None,
) {
@@ -1003,17 +999,3 @@ pub fn slice(input: TokenStream) -> TokenStream {
pub fn memo(input: TokenStream) -> TokenStream {
memo::memo_impl(input)
}
/// The `#[lazy]` macro marks an `async` function as a function that can be lazy-loaded from a
/// separate (WebAssembly) binary.
///
/// The first time the function is called, calling the function will first load that other binary,
/// then call the function. On subsequent call it will be called immediately, but still return
/// asynchronously to maintain the same API.
///
/// All parameters and output types should be concrete types, with no generics.
#[proc_macro_attribute]
#[proc_macro_error]
pub fn lazy(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
lazy::lazy_impl(args, s)
}

View File

@@ -13,13 +13,7 @@ pub fn params_impl(ast: &syn::DeriveInput) -> proc_macro::TokenStream {
.named
.iter()
.map(|field| {
let field_name_string = &field
.ident
.as_ref()
.expect("expected named struct fields")
.to_string()
.trim_start_matches("r#")
.to_owned();
let field_name_string = &field.ident.as_ref().expect("expected named struct fields").to_string();
let ident = &field.ident;
let ty = &field.ty;
let span = field.span();

View File

@@ -108,12 +108,9 @@ pub(crate) fn component_to_tokens(
let KeyedAttributeValue::Binding(binding) = &attr.possible_value
else {
if let Some(ident) = attr.key.to_string().strip_prefix("let:") {
let span = match &attr.key {
NodeName::Punctuated(path) => path[1].span(),
_ => unreachable!(),
};
let ident1 = format_ident!("{ident}", span = span);
return Some(quote_spanned! { span => #ident1 });
let ident1 =
format_ident!("{ident}", span = attr.key.span());
return Some(quote! { #ident1 });
} else {
return None;
}
@@ -171,7 +168,7 @@ pub(crate) fn component_to_tokens(
let spreads = (!(spreads.is_empty())).then(|| {
quote! {
.add_any_attr((#(#spreads,)*).into_attr())
.add_any_attr((#(#spreads,)*))
}
});

View File

@@ -197,7 +197,7 @@ enum InertElementBuilder<'a> {
},
}
impl ToTokens for InertElementBuilder<'_> {
impl<'a> ToTokens for InertElementBuilder<'a> {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
InertElementBuilder::GlobalClass { strs, .. } => {
@@ -219,7 +219,7 @@ enum GlobalClassItem<'a> {
String(String),
}
impl ToTokens for GlobalClassItem<'_> {
impl<'a> ToTokens for GlobalClassItem<'a> {
fn to_tokens(&self, tokens: &mut TokenStream) {
let addl_tokens = match self {
GlobalClassItem::Global(v) => v.to_token_stream(),
@@ -652,18 +652,6 @@ pub(crate) fn element_to_tokens(
},
_ => None,
};
if let NodeAttribute::Attribute(a) = a {
if let Some(Tuple(_)) = a.value() {
return Ordering::Greater;
}
}
if let NodeAttribute::Attribute(b) = b {
if let Some(Tuple(_)) = b.value() {
return Ordering::Less;
}
}
match (key_a.as_deref(), key_b.as_deref()) {
(Some("class"), Some("class")) | (Some("style"), Some("style")) => {
Ordering::Equal
@@ -767,7 +755,7 @@ pub(crate) fn element_to_tokens(
let name = node.name().to_string();
// link custom ident to name span for IDE docs
let custom = Ident::new("custom", name.span());
quote_spanned! { node.name().span() => ::leptos::tachys::html::element::#custom(#name) }
quote! { ::leptos::tachys::html::element::#custom(#name) }
} else if is_svg_element(&tag) {
parent_type = TagType::Svg;
let name = if tag == "use" || tag == "use_" {
@@ -775,33 +763,33 @@ pub(crate) fn element_to_tokens(
} else {
name.to_token_stream()
};
quote_spanned! { node.name().span() => ::leptos::tachys::svg::#name() }
quote! { ::leptos::tachys::svg::#name() }
} else if is_math_ml_element(&tag) {
parent_type = TagType::Math;
quote_spanned! { node.name().span() => ::leptos::tachys::mathml::#name() }
quote! { ::leptos::tachys::mathml::#name() }
} else if is_ambiguous_element(&tag) {
match parent_type {
TagType::Unknown => {
// We decided this warning was too aggressive, but I'll leave it here in case we want it later
/* proc_macro_error2::emit_warning!(name.span(), "The view macro is assuming this is an HTML element, \
but it is ambiguous; if it is an SVG or MathML element, prefix with svg:: or math::"); */
quote_spanned! { node.name().span() =>
quote! {
::leptos::tachys::html::element::#name()
}
}
TagType::Html => {
quote_spanned! { node.name().span() => ::leptos::tachys::html::element::#name() }
quote! { ::leptos::tachys::html::element::#name() }
}
TagType::Svg => {
quote_spanned! { node.name().span() => ::leptos::tachys::svg::#name() }
quote! { ::leptos::tachys::svg::#name() }
}
TagType::Math => {
quote_spanned! { node.name().span() => ::leptos::tachys::math::#name() }
quote! { ::leptos::tachys::math::#name() }
}
}
} else {
parent_type = TagType::Html;
quote_spanned! { name.span() => ::leptos::tachys::html::element::#name() }
quote! { ::leptos::tachys::html::element::#name() }
};
/* TODO restore this
@@ -1013,11 +1001,7 @@ pub(crate) fn attribute_absolute(
) -> Option<TokenStream> {
let key = node.key.to_string();
let contains_dash = key.contains('-');
let attr_colon = key.starts_with("attr:")
|| key.starts_with("style:")
|| key.starts_with("class:")
|| key.starts_with("prop:")
|| key.starts_with("use:");
let attr_colon = key.starts_with("attr:");
// anything that follows the x:y pattern
match &node.key {
NodeName::Punctuated(parts) if !contains_dash || attr_colon => {
@@ -1129,11 +1113,6 @@ pub(crate) fn attribute_absolute(
::leptos::tachys::html::attribute::custom::custom_attribute(#name, #value)
}
}
else if name == "node_ref" {
quote! {
::leptos::tachys::html::node_ref::#key(#value)
}
}
else {
quote! {
::leptos::tachys::html::attribute::#key(#value)
@@ -1656,7 +1635,7 @@ pub(crate) fn ident_from_tag_name(tag_name: &NodeName) -> Ident {
.path
.segments
.iter()
.next_back()
.last()
.map(|segment| segment.ident.clone())
.expect("element needs to have a name"),
NodeName::Block(_) => {
@@ -1727,7 +1706,7 @@ fn tuple_name(name: &str, node: &KeyedAttribute) -> TupleName {
TupleName::None
}
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug)]
enum TupleName {
None,
Str(String),

View File

@@ -1,15 +1,6 @@
use core::num::NonZeroUsize;
use leptos::prelude::*;
#[derive(PartialEq, Debug)]
struct UserInfo {
user_id: String,
email: String,
}
#[derive(PartialEq, Debug)]
struct Admin(bool);
#[component]
fn Component(
#[prop(optional)] optional: bool,
@@ -19,10 +10,6 @@ fn Component(
#[prop(default = NonZeroUsize::new(10).unwrap())] default: NonZeroUsize,
#[prop(into)] into: String,
impl_trait: impl Fn() -> i32 + 'static,
#[prop(name = "data")] UserInfo { email, user_id }: UserInfo,
#[prop(name = "tuple")] (name, id): (String, i32),
#[prop(name = "tuple_struct")] Admin(is_admin): Admin,
#[prop(name = "outside_name")] inside_name: i32,
) -> impl IntoView {
_ = optional;
_ = optional_into;
@@ -31,12 +18,6 @@ fn Component(
_ = default;
_ = into;
_ = impl_trait;
_ = email;
_ = user_id;
_ = id;
_ = name;
_ = is_admin;
_ = inside_name;
}
#[test]
@@ -45,13 +26,6 @@ fn component() {
.into("")
.strip_option(9)
.impl_trait(|| 42)
.data(UserInfo {
email: "em@il".into(),
user_id: "1".into(),
})
.tuple(("Joe".into(), 12))
.tuple_struct(Admin(true))
.outside_name(1)
.build();
assert!(!cp.optional);
assert_eq!(cp.optional_into, None);
@@ -60,16 +34,6 @@ fn component() {
assert_eq!(cp.default, NonZeroUsize::new(10).unwrap());
assert_eq!(cp.into, "");
assert_eq!((cp.impl_trait)(), 42);
assert_eq!(
cp.data,
UserInfo {
email: "em@il".into(),
user_id: "1".into(),
}
);
assert_eq!(cp.tuple, ("Joe".into(), 12));
assert_eq!(cp.tuple_struct, Admin(true));
assert_eq!(cp.outside_name, 1);
}
#[test]
@@ -81,26 +45,12 @@ fn component_nostrip() {
strip_option=9
into=""
impl_trait=|| 42
data=UserInfo {
email: "em@il".into(),
user_id: "1".into(),
}
tuple=("Joe".into(), 12)
tuple_struct=Admin(true)
outside_name=1
/>
<Component
nostrip:optional_into=Some("foo")
strip_option=9
into=""
impl_trait=|| 42
data=UserInfo {
email: "em@il".into(),
user_id: "1".into(),
}
tuple=("Joe".into(), 12)
tuple_struct=Admin(true)
outside_name=1
/>
};
}

View File

@@ -1,28 +0,0 @@
use leptos::prelude::*;
use leptos_router::params::Params;
#[derive(PartialEq, Debug, Params)]
struct UserInfo {
user_id: Option<String>,
email: Option<String>,
r#type: Option<i32>,
not_found: Option<i32>,
}
#[test]
fn params_test() {
let mut map = leptos_router::params::ParamsMap::new();
map.insert("user_id", "12".to_owned());
map.insert("email", "em@il".to_owned());
map.insert("type", "12".to_owned());
let user_info = UserInfo::from_map(&map).unwrap();
assert_eq!(
UserInfo {
email: Some("em@il".to_owned()),
user_id: Some("12".to_owned()),
r#type: Some(12),
not_found: None,
},
user_info
);
}

View File

@@ -44,10 +44,4 @@ fn default_with_invalid_value(
_ = default;
}
#[component]
fn destructure_without_name((default, value): (bool, i32)) -> impl IntoView {
_ = default;
_ = value;
}
fn main() {}

View File

@@ -1,4 +1,4 @@
error: supported fields are `optional`, `optional_no_strip`, `strip_option`, `default`, `into`, `attrs` and `name`
error: supported fields are `optional`, `optional_no_strip`, `strip_option`, `default`, `into` and `attrs`
--> tests/ui/component.rs:10:31
|
10 | fn unknown_prop_option(#[prop(hello)] test: bool) -> impl IntoView {
@@ -41,9 +41,3 @@ error: unexpected end of input, expected one of: identifier, `::`, `<`, `_`, lit
| ^^^^^^^^^^^^
|
= note: this error originates in the attribute macro `component` (in Nightly builds, run with -Z macro-backtrace for more info)
error: destructured props must be given a name e.g. #[prop(name = "data")]
--> tests/ui/component.rs:48:29
|
48 | fn destructure_without_name((default, value): (bool, i32)) -> impl IntoView {
| ^^^^^^^^^^^^^^^^

View File

@@ -1,4 +1,4 @@
error: supported fields are `optional`, `optional_no_strip`, `strip_option`, `default`, `into`, `attrs` and `name`
error: supported fields are `optional`, `optional_no_strip`, `strip_option`, `default`, `into` and `attrs`
--> tests/ui/component_absolute.rs:5:31
|
5 | fn unknown_prop_option(#[prop(hello)] test: bool) -> impl ::leptos::IntoView {

View File

@@ -11,11 +11,11 @@ edition.workspace = true
[dependencies]
base64 = "0.22.1"
codee = { version = "0.3.0", features = ["json_serde"] }
codee = { version = "0.2.0", features = ["json_serde"] }
hydration_context = { workspace = true }
reactive_graph = { workspace = true, features = ["hydration"] }
server_fn = { workspace = true }
tracing = { version = "0.1.41", optional = true }
tracing = { version = "0.1.40", optional = true }
futures = "0.3.31"
any_spawner = { workspace = true }
@@ -25,8 +25,8 @@ send_wrapper = "0.6"
# serialization formats
serde = { version = "1.0" }
js-sys = { version = "0.3.74", optional = true }
wasm-bindgen = { version = "0.2.97", optional = true }
js-sys = { version = "0.3.72", optional = true }
wasm-bindgen = { version = "0.2.95", optional = true }
serde_json = { version = "1.0" }
[features]
@@ -44,6 +44,3 @@ denylist = ["tracing"]
[package.metadata.docs.rs]
rustdoc-args = ["--generate-link-to-definition"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }

View File

@@ -3,13 +3,9 @@ use reactive_graph::{
owner::use_context,
traits::DefinedAt,
};
use server_fn::{error::FromServerFnError, ServerFn};
use server_fn::{error::ServerFnErrorSerde, ServerFn, ServerFnError};
use std::{ops::Deref, panic::Location, sync::Arc};
/// An error that can be caused by a server action.
///
/// This is used for propagating errors from the server to the client when JS/WASM are not
/// supported.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ServerActionError {
path: Arc<str>,
@@ -17,7 +13,6 @@ pub struct ServerActionError {
}
impl ServerActionError {
/// Creates a new error associated with the given path.
pub fn new(path: &str, err: &str) -> Self {
Self {
path: path.into(),
@@ -25,25 +20,22 @@ impl ServerActionError {
}
}
/// The path with which this error is associated.
pub fn path(&self) -> &str {
&self.path
}
/// The error message.
pub fn err(&self) -> &str {
&self.err
}
}
/// An [`ArcAction`] that can be used to call a server function.
pub struct ArcServerAction<S>
where
S: ServerFn + 'static,
S::Output: 'static,
{
inner: ArcAction<S, Result<S::Output, S::Error>>,
#[cfg(any(debug_assertions, leptos_debuginfo))]
inner: ArcAction<S, Result<S::Output, ServerFnError<S::Error>>>,
#[cfg(debug_assertions)]
defined_at: &'static Location<'static>,
}
@@ -52,21 +44,19 @@ where
S: ServerFn + Clone + Send + Sync + 'static,
S::Output: Send + Sync + 'static,
S::Error: Send + Sync + 'static,
S::Error: FromServerFnError,
{
/// Creates a new [`ArcAction`] that will call the server function `S` when dispatched.
#[track_caller]
pub fn new() -> Self {
let err = use_context::<ServerActionError>().and_then(|error| {
(error.path() == S::PATH)
.then(|| S::Error::de(error.err()))
.then(|| ServerFnError::<S::Error>::de(error.err()))
.map(Err)
});
Self {
inner: ArcAction::new_with_value(err, |input: &S| {
S::run_on_client(input.clone())
}),
#[cfg(any(debug_assertions, leptos_debuginfo))]
#[cfg(debug_assertions)]
defined_at: Location::caller(),
}
}
@@ -77,7 +67,7 @@ where
S: ServerFn + 'static,
S::Output: 'static,
{
type Target = ArcAction<S, Result<S::Output, S::Error>>;
type Target = ArcAction<S, Result<S::Output, ServerFnError<S::Error>>>;
fn deref(&self) -> &Self::Target {
&self.inner
@@ -92,7 +82,7 @@ where
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
#[cfg(any(debug_assertions, leptos_debuginfo))]
#[cfg(debug_assertions)]
defined_at: self.defined_at,
}
}
@@ -115,25 +105,24 @@ where
S::Output: 'static,
{
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(any(debug_assertions, leptos_debuginfo))]
#[cfg(debug_assertions)]
{
Some(self.defined_at)
}
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
#[cfg(not(debug_assertions))]
{
None
}
}
}
/// An [`Action`] that can be used to call a server function.
pub struct ServerAction<S>
where
S: ServerFn + 'static,
S::Output: 'static,
{
inner: Action<S, Result<S::Output, S::Error>>,
#[cfg(any(debug_assertions, leptos_debuginfo))]
inner: Action<S, Result<S::Output, ServerFnError<S::Error>>>,
#[cfg(debug_assertions)]
defined_at: &'static Location<'static>,
}
@@ -143,18 +132,17 @@ where
S::Output: Send + Sync + 'static,
S::Error: Send + Sync + 'static,
{
/// Creates a new [`Action`] that will call the server function `S` when dispatched.
pub fn new() -> Self {
let err = use_context::<ServerActionError>().and_then(|error| {
(error.path() == S::PATH)
.then(|| S::Error::de(error.err()))
.then(|| ServerFnError::<S::Error>::de(error.err()))
.map(Err)
});
Self {
inner: Action::new_with_value(err, |input: &S| {
S::run_on_client(input.clone())
}),
#[cfg(any(debug_assertions, leptos_debuginfo))]
#[cfg(debug_assertions)]
defined_at: Location::caller(),
}
}
@@ -183,14 +171,15 @@ where
S::Output: Send + Sync + 'static,
S::Error: Send + Sync + 'static,
{
type Target = Action<S, Result<S::Output, S::Error>>;
type Target = Action<S, Result<S::Output, ServerFnError<S::Error>>>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl<S> From<ServerAction<S>> for Action<S, Result<S::Output, S::Error>>
impl<S> From<ServerAction<S>>
for Action<S, Result<S::Output, ServerFnError<S::Error>>>
where
S: ServerFn + 'static,
S::Output: 'static,
@@ -217,11 +206,11 @@ where
S::Output: 'static,
{
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(any(debug_assertions, leptos_debuginfo))]
#[cfg(debug_assertions)]
{
Some(self.defined_at)
}
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
#[cfg(not(debug_assertions))]
{
None
}

View File

@@ -1,6 +1,4 @@
//! Utilities for communicating between the server and the client with Leptos.
#![deny(missing_docs)]
//#![deny(missing_docs)]
#![forbid(unsafe_code)]
mod action;
@@ -15,25 +13,135 @@ pub use once_resource::*;
mod resource;
pub use resource::*;
mod shared;
////! # Leptos Server Functions
////!
////! This package is based on a simple idea: sometimes its useful to write functions
////! that will only run on the server, and call them from the client.
////!
////! If youre creating anything beyond a toy app, youll need to do this all the time:
////! reading from or writing to a database that only runs on the server, running expensive
////! computations using libraries you dont want to ship down to the client, accessing
////! APIs that need to be called from the server rather than the client for CORS reasons
////! or because you need a secret API key thats stored on the server and definitely
////! shouldnt be shipped down to a users browser.
////!
////! Traditionally, this is done by separating your server and client code, and by setting
////! up something like a REST API or GraphQL API to allow your client to fetch and mutate
////! data on the server. This is fine, but it requires you to write and maintain your code
////! in multiple separate places (client-side code for fetching, server-side functions to run),
////! as well as creating a third thing to manage, which is the API contract between the two.
////!
////! This package provides two simple primitives that allow you instead to write co-located,
////! isomorphic server functions. (*Co-located* means you can write them in your app code so
////! that they are “located alongside” the client code that calls them, rather than separating
////! the client and server sides. *Isomorphic* means you can call them from the client as if
////! you were simply calling a function; the function call has the “same shape” on the client
////! as it does on the server.)
////!
////! ### `#[server]`
////!
////! The [`#[server]`](https://docs.rs/leptos/latest/leptos/attr.server.html) macro allows you to annotate a function to
////! indicate that it should only run on the server (i.e., when you have an `ssr` feature in your
////! crate that is enabled).
////!
////! ```rust,ignore
////! use leptos::prelude::*;
////! #[server(ReadFromDB)]
////! async fn read_posts(how_many: usize, query: String) -> Result<Vec<Posts>, ServerFnError> {
////! // do some server-only work here to access the database
////! let posts = todo!();;
////! Ok(posts)
////! }
////!
////! // call the function
////! spawn_local(async {
////! let posts = read_posts(3, "my search".to_string()).await;
////! log::debug!("posts = {posts:#?}");
////! });
////! ```
////!
////! If you call this function from the client, it will serialize the function arguments and `POST`
////! them to the server as if they were the inputs in `<form method="POST">`.
////!
////! Heres what you need to remember:
////! - **Server functions must be `async`.** Even if the work being done inside the function body
////! can run synchronously on the server, from the clients perspective it involves an asynchronous
////! function call.
////! - **Server functions must return `Result<T, ServerFnError>`.** Even if the work being done
////! inside the function body cant fail, the processes of serialization/deserialization and the
////! network call are fallible.
////! - **Return types must be [Serializable](leptos_reactive::Serializable).**
////! This should be fairly obvious: we have to serialize arguments to send them to the server, and we
////! need to deserialize the result to return it to the client.
////! - **Arguments must be implement [serde::Serialize].** They are serialized as an `application/x-www-form-urlencoded`
////! form data using [`serde_qs`](https://docs.rs/serde_qs/latest/serde_qs/) or as `application/cbor`
////! using [`cbor`](https://docs.rs/cbor/latest/cbor/). **Note**: You should explicitly include `serde` with the
////! `derive` feature enabled in your `Cargo.toml`. You can do this by running `cargo add serde --features=derive`.
////! - Context comes from the server. [`use_context`](leptos_reactive::use_context) can be used to access specific
////! server-related data, as documented in the server integrations. This allows accessing things like HTTP request
////! headers as needed. However, server functions *not* have access to reactive state that exists in the client.
////!
////! ## Server Function Encodings
////!
////! By default, the server function call is a `POST` request that serializes the arguments as URL-encoded form data in the body
////! of the request. But there are a few other methods supported. Optionally, we can provide another argument to the `#[server]`
////! macro to specify an alternate encoding:
////!
////! ```rust,ignore
////! #[server(AddTodo, "/api", "Url")]
////! #[server(AddTodo, "/api", "GetJson")]
////! #[server(AddTodo, "/api", "Cbor")]
////! #[server(AddTodo, "/api", "GetCbor")]
////! ```
////!
////! The four options use different combinations of HTTP verbs and encoding methods:
////!
////! | Name | Method | Request | Response |
////! | ----------------- | ------ | ----------- | -------- |
////! | **Url** (default) | POST | URL encoded | JSON |
////! | **GetJson** | GET | URL encoded | JSON |
////! | **Cbor** | POST | CBOR | CBOR |
////! | **GetCbor** | GET | URL encoded | CBOR |
////!
////! In other words, you have two choices:
////!
////! - `GET` or `POST`? This has implications for things like browser or CDN caching; while `POST` requests should not be cached,
////! `GET` requests can be.
////! - Plain text (arguments sent with URL/form encoding, results sent as JSON) or a binary format (CBOR, encoded as a base64
////! string)?
////!
////! ## Why not `PUT` or `DELETE`? Why URL/form encoding, and not JSON?**
////!
////! These are reasonable questions. Much of the web is built on REST API patterns that encourage the use of semantic HTTP
////! methods like `DELETE` to delete an item from a database, and many devs are accustomed to sending data to APIs in the
////! JSON format.
////!
////! The reason we use `POST` or `GET` with URL-encoded data by default is the `<form>` support. For better or for worse,
////! HTML forms dont support `PUT` or `DELETE`, and they dont support sending JSON. This means that if you use anything
////! but a `GET` or `POST` request with URL-encoded data, it can only work once WASM has loaded.
////!
////! The CBOR encoding is supported for historical reasons; an earlier version of server functions used a URL encoding that
////! didnt support nested objects like structs or vectors as server function arguments, which CBOR did. But note that the
////! CBOR forms encounter the same issue as `PUT`, `DELETE`, or JSON: they do not degrade gracefully if the WASM version of
////! your app is not available.
//pub use server_fn::{error::ServerFnErrorErr, ServerFnError};
//mod action;
//mod multi_action;
//pub use action::*;
//pub use multi_action::*;
//extern crate tracing;
use base64::{engine::general_purpose::STANDARD_NO_PAD, DecodeError, Engine};
pub use shared::*;
/// Encodes data into a string.
pub trait IntoEncodedString {
/// Encodes the data.
fn into_encoded_string(self) -> String;
}
/// Decodes data from a string.
pub trait FromEncodedStr {
/// The decoded data.
type DecodedType<'a>: Borrow<Self>;
/// The type of an error encountered during decoding.
type DecodingError;
/// Decodes the string.
fn from_encoded_str(
data: &str,
) -> Result<Self::DecodedType<'_>, Self::DecodingError>;
@@ -79,13 +187,12 @@ mod view_implementations {
use reactive_graph::traits::Read;
use std::future::Future;
use tachys::{
html::attribute::{any_attribute::AnyAttribute, Attribute},
html::attribute::Attribute,
hydration::Cursor,
reactive_graph::{RenderEffectState, Suspend, SuspendState},
ssr::StreamBuilder,
view::{
add_attr::AddAnyAttr, any_view::ExtraAttrsMut, Position,
PositionState, Render, RenderHtml,
add_attr::AddAnyAttr, Position, PositionState, Render, RenderHtml,
},
};
@@ -96,17 +203,12 @@ mod view_implementations {
{
type State = RenderEffectState<SuspendState<T>>;
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
(move || Suspend::new(async move { self.await })).build(extra_attrs)
fn build(self) -> Self::State {
(move || Suspend::new(async move { self.await })).build()
}
fn rebuild(
self,
state: &mut Self::State,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
(move || Suspend::new(async move { self.await }))
.rebuild(state, extra_attrs)
fn rebuild(self, state: &mut Self::State) {
(move || Suspend::new(async move { self.await })).rebuild(state)
}
}
@@ -141,20 +243,15 @@ mod view_implementations {
Ser: Send + 'static,
{
type AsyncOutput = Option<T>;
type Owned = Self;
const MIN_LENGTH: usize = 0;
fn dry_resolve(&mut self, _extra_attrs: ExtraAttrsMut<'_>) {
fn dry_resolve(&mut self) {
self.read();
}
fn resolve(
self,
extra_attrs: ExtraAttrsMut<'_>,
) -> impl Future<Output = Self::AsyncOutput> + Send {
(move || Suspend::new(async move { self.await }))
.resolve(extra_attrs)
fn resolve(self) -> impl Future<Output = Self::AsyncOutput> + Send {
(move || Suspend::new(async move { self.await })).resolve()
}
fn to_html_with_buf(
@@ -163,14 +260,12 @@ mod view_implementations {
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
(move || Suspend::new(async move { self.await })).to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs,
);
}
@@ -180,7 +275,6 @@ mod view_implementations {
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) where
Self: Sized,
{
@@ -190,7 +284,6 @@ mod view_implementations {
position,
escape,
mark_branches,
extra_attrs,
);
}
@@ -198,14 +291,9 @@ mod view_implementations {
self,
cursor: &Cursor,
position: &PositionState,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
(move || Suspend::new(async move { self.await }))
.hydrate::<FROM_SERVER>(cursor, position, extra_attrs)
}
fn into_owned(self) -> Self::Owned {
self
.hydrate::<FROM_SERVER>(cursor, position)
}
}
}

View File

@@ -8,11 +8,8 @@ use reactive_graph::{
ToAnySource, ToAnySubscriber,
},
owner::use_context,
signal::{
guards::{AsyncPlain, ReadGuard},
ArcRwSignal, RwSignal,
},
traits::{DefinedAt, IsDisposed, ReadUntracked, Track, Update, Write},
signal::guards::{AsyncPlain, ReadGuard},
traits::{DefinedAt, IsDisposed, ReadUntracked},
};
use send_wrapper::SendWrapper;
use std::{
@@ -20,11 +17,9 @@ use std::{
panic::Location,
};
/// A reference-counted resource that only loads its data locally on the client.
pub struct ArcLocalResource<T> {
data: ArcAsyncDerived<SendWrapper<T>>,
refetch: ArcRwSignal<usize>,
#[cfg(any(debug_assertions, leptos_debuginfo))]
#[cfg(debug_assertions)]
defined_at: &'static Location<'static>,
}
@@ -32,18 +27,13 @@ impl<T> Clone for ArcLocalResource<T> {
fn clone(&self) -> Self {
Self {
data: self.data.clone(),
refetch: self.refetch.clone(),
#[cfg(any(debug_assertions, leptos_debuginfo))]
#[cfg(debug_assertions)]
defined_at: self.defined_at,
}
}
}
impl<T> ArcLocalResource<T> {
/// Creates the resource.
///
/// This will only begin loading data if you are on the client (i.e., if you do not have the
/// `ssr` feature activated).
#[track_caller]
pub fn new<Fut>(fetcher: impl Fn() -> Fut + 'static) -> Self
where
@@ -70,27 +60,15 @@ impl<T> ArcLocalResource<T> {
}
};
let fetcher = SendWrapper::new(fetcher);
let refetch = ArcRwSignal::new(0);
let data = {
let refetch = refetch.clone();
ArcAsyncDerived::new(move || {
refetch.track();
Self {
data: ArcAsyncDerived::new(move || {
let fut = fetcher();
SendWrapper::new(async move { SendWrapper::new(fut.await) })
})
};
Self {
data,
refetch,
#[cfg(any(debug_assertions, leptos_debuginfo))]
}),
#[cfg(debug_assertions)]
defined_at: Location::caller(),
}
}
/// Re-runs the async function.
pub fn refetch(&self) {
*self.refetch.write() += 1;
}
}
impl<T> IntoFuture for ArcLocalResource<T>
@@ -121,11 +99,11 @@ where
impl<T> DefinedAt for ArcLocalResource<T> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(any(debug_assertions, leptos_debuginfo))]
#[cfg(debug_assertions)]
{
Some(self.defined_at)
}
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
#[cfg(not(debug_assertions))]
{
None
}
@@ -214,11 +192,9 @@ impl<T> Subscriber for ArcLocalResource<T> {
}
}
/// A resource that only loads its data locally on the client.
pub struct LocalResource<T> {
data: AsyncDerived<SendWrapper<T>>,
refetch: RwSignal<usize>,
#[cfg(any(debug_assertions, leptos_debuginfo))]
#[cfg(debug_assertions)]
defined_at: &'static Location<'static>,
}
@@ -231,10 +207,6 @@ impl<T> Clone for LocalResource<T> {
impl<T> Copy for LocalResource<T> {}
impl<T> LocalResource<T> {
/// Creates the resource.
///
/// This will only begin loading data if you are on the client (i.e., if you do not have the
/// `ssr` feature activated).
#[track_caller]
pub fn new<Fut>(fetcher: impl Fn() -> Fut + 'static) -> Self
where
@@ -260,7 +232,6 @@ impl<T> LocalResource<T> {
}
}
};
let refetch = RwSignal::new(0);
Self {
data: if cfg!(feature = "ssr") {
@@ -268,21 +239,14 @@ impl<T> LocalResource<T> {
} else {
let fetcher = SendWrapper::new(fetcher);
AsyncDerived::new(move || {
refetch.track();
let fut = fetcher();
SendWrapper::new(async move { SendWrapper::new(fut.await) })
})
},
refetch,
#[cfg(any(debug_assertions, leptos_debuginfo))]
#[cfg(debug_assertions)]
defined_at: Location::caller(),
}
}
/// Re-runs the async function.
pub fn refetch(&self) {
self.refetch.try_update(|n| *n += 1);
}
}
impl<T> IntoFuture for LocalResource<T>
@@ -313,11 +277,11 @@ where
impl<T> DefinedAt for LocalResource<T> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(any(debug_assertions, leptos_debuginfo))]
#[cfg(debug_assertions)]
{
Some(self.defined_at)
}
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
#[cfg(not(debug_assertions))]
{
None
}
@@ -424,8 +388,7 @@ impl<T: 'static> From<ArcLocalResource<T>> for LocalResource<T> {
fn from(arc: ArcLocalResource<T>) -> Self {
Self {
data: arc.data.into(),
refetch: arc.refetch.into(),
#[cfg(any(debug_assertions, leptos_debuginfo))]
#[cfg(debug_assertions)]
defined_at: arc.defined_at,
}
}
@@ -435,8 +398,7 @@ impl<T: 'static> From<LocalResource<T>> for ArcLocalResource<T> {
fn from(local: LocalResource<T>) -> Self {
Self {
data: local.data.into(),
refetch: local.refetch.into(),
#[cfg(any(debug_assertions, leptos_debuginfo))]
#[cfg(debug_assertions)]
defined_at: local.defined_at,
}
}

View File

@@ -2,17 +2,16 @@ use reactive_graph::{
actions::{ArcMultiAction, MultiAction},
traits::DefinedAt,
};
use server_fn::ServerFn;
use server_fn::{ServerFn, ServerFnError};
use std::{ops::Deref, panic::Location};
/// An [`ArcMultiAction`] that can be used to call a server function.
pub struct ArcServerMultiAction<S>
where
S: ServerFn + 'static,
S::Output: 'static,
{
inner: ArcMultiAction<S, Result<S::Output, S::Error>>,
#[cfg(any(debug_assertions, leptos_debuginfo))]
inner: ArcMultiAction<S, Result<S::Output, ServerFnError<S::Error>>>,
#[cfg(debug_assertions)]
defined_at: &'static Location<'static>,
}
@@ -22,14 +21,13 @@ where
S::Output: Send + Sync + 'static,
S::Error: Send + Sync + 'static,
{
/// Creates a new [`ArcMultiAction`] which, when dispatched, will call the server function `S`.
#[track_caller]
pub fn new() -> Self {
Self {
inner: ArcMultiAction::new(|input: &S| {
S::run_on_client(input.clone())
}),
#[cfg(any(debug_assertions, leptos_debuginfo))]
#[cfg(debug_assertions)]
defined_at: Location::caller(),
}
}
@@ -40,7 +38,7 @@ where
S: ServerFn + 'static,
S::Output: 'static,
{
type Target = ArcMultiAction<S, Result<S::Output, S::Error>>;
type Target = ArcMultiAction<S, Result<S::Output, ServerFnError<S::Error>>>;
fn deref(&self) -> &Self::Target {
&self.inner
@@ -55,7 +53,7 @@ where
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
#[cfg(any(debug_assertions, leptos_debuginfo))]
#[cfg(debug_assertions)]
defined_at: self.defined_at,
}
}
@@ -78,30 +76,29 @@ where
S::Output: 'static,
{
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(any(debug_assertions, leptos_debuginfo))]
#[cfg(debug_assertions)]
{
Some(self.defined_at)
}
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
#[cfg(not(debug_assertions))]
{
None
}
}
}
/// A [`MultiAction`] that can be used to call a server function.
pub struct ServerMultiAction<S>
where
S: ServerFn + 'static,
S::Output: 'static,
{
inner: MultiAction<S, Result<S::Output, S::Error>>,
#[cfg(any(debug_assertions, leptos_debuginfo))]
inner: MultiAction<S, Result<S::Output, ServerFnError<S::Error>>>,
#[cfg(debug_assertions)]
defined_at: &'static Location<'static>,
}
impl<S> From<ServerMultiAction<S>>
for MultiAction<S, Result<S::Output, S::Error>>
for MultiAction<S, Result<S::Output, ServerFnError<S::Error>>>
where
S: ServerFn + 'static,
S::Output: 'static,
@@ -117,13 +114,12 @@ where
S::Output: Send + Sync + 'static,
S::Error: Send + Sync + 'static,
{
/// Creates a new [`MultiAction`] which, when dispatched, will call the server function `S`.
pub fn new() -> Self {
Self {
inner: MultiAction::new(|input: &S| {
S::run_on_client(input.clone())
}),
#[cfg(any(debug_assertions, leptos_debuginfo))]
#[cfg(debug_assertions)]
defined_at: Location::caller(),
}
}
@@ -152,7 +148,7 @@ where
S::Output: 'static,
S::Error: 'static,
{
type Target = MultiAction<S, Result<S::Output, S::Error>>;
type Target = MultiAction<S, Result<S::Output, ServerFnError<S::Error>>>;
fn deref(&self) -> &Self::Target {
&self.inner
@@ -176,11 +172,11 @@ where
S::Output: 'static,
{
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(any(debug_assertions, leptos_debuginfo))]
#[cfg(debug_assertions)]
{
Some(self.defined_at)
}
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
#[cfg(not(debug_assertions))]
{
None
}

View File

@@ -0,0 +1,344 @@
use leptos_reactive::{
is_suppressing_resource_load, signal_prelude::*, spawn_local, store_value,
untrack, StoredValue,
};
use server_fn::{ServerFn, ServerFnError};
use std::{future::Future, pin::Pin, rc::Rc};
/// An action that synchronizes multiple imperative `async` calls to the reactive system,
/// tracking the progress of each one.
///
/// Where an [Action](crate::Action) fires a single call, a `MultiAction` allows you to
/// keep track of multiple in-flight actions.
///
/// If youre trying to load data by running an `async` function reactively, you probably
/// want to use a [Resource](leptos_reactive::Resource) instead. If youre trying to occasionally
/// run an `async` function in response to something like a user adding a task to a todo list,
/// youre in the right place.
///
/// ```rust
/// # use leptos::*;
/// # let runtime = create_runtime();
/// async fn send_new_todo_to_api(task: String) -> usize {
/// // do something...
/// // return a task id
/// 42
/// }
/// let add_todo = create_multi_action(|task: &String| {
/// // `task` is given as `&String` because its value is available in `input`
/// send_new_todo_to_api(task.clone())
/// });
///
/// # if false {
/// add_todo.dispatch("Buy milk".to_string());
/// add_todo.dispatch("???".to_string());
/// add_todo.dispatch("Profit!!!".to_string());
/// # }
///
/// # runtime.dispose();
/// ```
///
/// The input to the `async` function should always be a single value,
/// but it can be of any type. The argument is always passed by reference to the
/// function, because it is stored in [Submission::input] as well.
///
/// ```rust
/// # use leptos::*;
/// # let runtime = create_runtime();
/// // if there's a single argument, just use that
/// let action1 = create_multi_action(|input: &String| {
/// let input = input.clone();
/// async move { todo!() }
/// });
///
/// // if there are no arguments, use the unit type `()`
/// let action2 = create_multi_action(|input: &()| async { todo!() });
///
/// // if there are multiple arguments, use a tuple
/// let action3 =
/// create_multi_action(|input: &(usize, String)| async { todo!() });
/// # runtime.dispose();
/// ```
pub struct MultiAction<I, O>(StoredValue<MultiActionState<I, O>>)
where
I: 'static,
O: 'static;
impl<I, O> MultiAction<I, O>
where
I: 'static,
O: 'static,
{
}
impl<I, O> Clone for MultiAction<I, O>
where
I: 'static,
O: 'static,
{
fn clone(&self) -> Self {
*self
}
}
impl<I, O> Copy for MultiAction<I, O>
where
I: 'static,
O: 'static,
{
}
impl<I, O> MultiAction<I, O>
where
I: 'static,
O: 'static,
{
/// Calls the `async` function with a reference to the input type as its argument.
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", skip_all)
)]
pub fn dispatch(&self, input: I) {
self.0.with_value(|a| a.dispatch(input))
}
/// The set of all submissions to this multi-action.
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", skip_all)
)]
pub fn submissions(&self) -> ReadSignal<Vec<Submission<I, O>>> {
self.0.with_value(|a| a.submissions())
}
/// The URL associated with the action (typically as part of a server function.)
/// This enables integration with the `MultiActionForm` component in `leptos_router`.
pub fn url(&self) -> Option<String> {
self.0.with_value(|a| a.url.as_ref().cloned())
}
/// How many times an action has successfully resolved.
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", skip_all)
)]
pub fn version(&self) -> RwSignal<usize> {
self.0.with_value(|a| a.version)
}
/// Associates the URL of the given server function with this action.
/// This enables integration with the `MultiActionForm` component in `leptos_router`.
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", skip_all)
)]
pub fn using_server_fn<T: ServerFn>(self) -> Self {
self.0.update_value(|a| {
a.url = Some(T::url().to_string());
});
self
}
}
struct MultiActionState<I, O>
where
I: 'static,
O: 'static,
{
/// How many times an action has successfully resolved.
pub version: RwSignal<usize>,
submissions: RwSignal<Vec<Submission<I, O>>>,
url: Option<String>,
#[allow(clippy::complexity)]
action_fn: Rc<dyn Fn(&I) -> Pin<Box<dyn Future<Output = O>>>>,
}
/// An action that has been submitted by dispatching it to a [MultiAction](crate::MultiAction).
pub struct Submission<I, O>
where
I: 'static,
O: 'static,
{
/// The current argument that was dispatched to the `async` function.
/// `Some` while we are waiting for it to resolve, `None` if it has resolved.
pub input: RwSignal<Option<I>>,
/// The most recent return value of the `async` function.
pub value: RwSignal<Option<O>>,
pub(crate) pending: RwSignal<bool>,
/// Controls this submission has been canceled.
pub canceled: RwSignal<bool>,
}
impl<I, O> Clone for Submission<I, O> {
fn clone(&self) -> Self {
*self
}
}
impl<I, O> Copy for Submission<I, O> {}
impl<I, O> Submission<I, O>
where
I: 'static,
O: 'static,
{
/// Whether this submission is currently waiting to resolve.
pub fn pending(&self) -> ReadSignal<bool> {
self.pending.read_only()
}
/// Cancels the submission, preventing it from resolving.
pub fn cancel(&self) {
self.canceled.set(true);
}
}
impl<I, O> MultiActionState<I, O>
where
I: 'static,
O: 'static,
{
/// Calls the `async` function with a reference to the input type as its argument.
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", skip_all)
)]
pub fn dispatch(&self, input: I) {
if !is_suppressing_resource_load() {
let fut = (self.action_fn)(&input);
let submission = Submission {
input: create_rw_signal(Some(input)),
value: create_rw_signal(None),
pending: create_rw_signal(true),
canceled: create_rw_signal(false),
};
self.submissions.update(|subs| subs.push(submission));
let canceled = submission.canceled;
let input = submission.input;
let pending = submission.pending;
let value = submission.value;
let version = self.version;
spawn_local(async move {
let new_value = fut.await;
let canceled = untrack(move || canceled.get());
if !canceled {
value.set(Some(new_value));
}
input.set(None);
pending.set(false);
version.update(|n| *n += 1);
})
}
}
/// The set of all submissions to this multi-action.
pub fn submissions(&self) -> ReadSignal<Vec<Submission<I, O>>> {
self.submissions.read_only()
}
}
/// Creates an [MultiAction] to synchronize an imperative `async` call to the synchronous reactive system.
///
/// If youre trying to load data by running an `async` function reactively, you probably
/// want to use a [create_resource](leptos_reactive::create_resource) instead. If youre trying
/// to occasionally run an `async` function in response to something like a user clicking a button,
/// you're in the right place.
///
/// ```rust
/// # use leptos::*;
/// # let runtime = create_runtime();
/// async fn send_new_todo_to_api(task: String) -> usize {
/// // do something...
/// // return a task id
/// 42
/// }
/// let add_todo = create_multi_action(|task: &String| {
/// // `task` is given as `&String` because its value is available in `input`
/// send_new_todo_to_api(task.clone())
/// });
/// # if false {
///
/// add_todo.dispatch("Buy milk".to_string());
/// add_todo.dispatch("???".to_string());
/// add_todo.dispatch("Profit!!!".to_string());
///
/// assert_eq!(add_todo.submissions().get().len(), 3);
/// # }
/// # runtime.dispose();
/// ```
///
/// The input to the `async` function should always be a single value,
/// but it can be of any type. The argument is always passed by reference to the
/// function, because it is stored in [Submission::input] as well.
///
/// ```rust
/// # use leptos::*;
/// # let runtime = create_runtime();
/// // if there's a single argument, just use that
/// let action1 = create_multi_action(|input: &String| {
/// let input = input.clone();
/// async move { todo!() }
/// });
///
/// // if there are no arguments, use the unit type `()`
/// let action2 = create_multi_action(|input: &()| async { todo!() });
///
/// // if there are multiple arguments, use a tuple
/// let action3 =
/// create_multi_action(|input: &(usize, String)| async { todo!() });
/// # runtime.dispose();
/// ```
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
pub fn create_multi_action<I, O, F, Fu>(action_fn: F) -> MultiAction<I, O>
where
I: 'static,
O: 'static,
F: Fn(&I) -> Fu + 'static,
Fu: Future<Output = O> + 'static,
{
let version = create_rw_signal(0);
let submissions = create_rw_signal(Vec::new());
let action_fn = Rc::new(move |input: &I| {
let fut = action_fn(input);
Box::pin(fut) as Pin<Box<dyn Future<Output = O>>>
});
MultiAction(store_value(MultiActionState {
version,
submissions,
url: None,
action_fn,
}))
}
/// Creates an [MultiAction] that can be used to call a server function.
///
/// ```rust,ignore
/// # use leptos::*;
///
/// #[server(MyServerFn)]
/// async fn my_server_fn() -> Result<(), ServerFnError> {
/// todo!()
/// }
///
/// # let runtime = create_runtime();
/// let my_server_multi_action = create_server_multi_action::<MyServerFn>();
/// # runtime.dispose();
/// ```
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
pub fn create_server_multi_action<S>(
) -> MultiAction<S, Result<S::Output, ServerFnError<S::Error>>>
where
S: Clone + ServerFn,
{
#[cfg(feature = "ssr")]
let c = move |args: &S| S::run_body(args.clone());
#[cfg(not(feature = "ssr"))]
let c = move |args: &S| S::run_on_client(args.clone());
create_multi_action(c).using_server_fn::<S>()
}

View File

@@ -43,15 +43,6 @@ use std::{
task::{Context, Poll, Waker},
};
/// A reference-counted resource that only loads once.
///
/// Resources allow asynchronously loading data and serializing it from the server to the client,
/// so that it loads on the server, and is then deserialized on the client. This improves
/// performance by beginning data loading on the server when the request is made, rather than
/// beginning it on the client after WASM has been loaded.
///
/// You can access the value of the resource either synchronously using `.get()` or asynchronously
/// using `.await`.
#[derive(Debug)]
pub struct ArcOnceResource<T, Ser = JsonSerdeCodec> {
trigger: ArcTrigger,
@@ -60,7 +51,7 @@ pub struct ArcOnceResource<T, Ser = JsonSerdeCodec> {
suspenses: Arc<RwLock<Vec<SuspenseContext>>>,
loading: Arc<AtomicBool>,
ser: PhantomData<fn() -> Ser>,
#[cfg(any(debug_assertions, leptos_debuginfo))]
#[cfg(debug_assertions)]
defined_at: &'static Location<'static>,
}
@@ -73,7 +64,7 @@ impl<T, Ser> Clone for ArcOnceResource<T, Ser> {
suspenses: self.suspenses.clone(),
loading: self.loading.clone(),
ser: self.ser,
#[cfg(any(debug_assertions, leptos_debuginfo))]
#[cfg(debug_assertions)]
defined_at: self.defined_at,
}
}
@@ -89,12 +80,6 @@ where
<Ser as Encoder<T>>::Encoded: IntoEncodedString,
<Ser as Decoder<T>>::Encoded: FromEncodedStr,
{
/// Creates a new resource with the encoding `Ser`. If `blocking` is `true`, this is a blocking
/// resource.
///
/// Blocking resources prevent any of the HTTP response from being sent until they have loaded.
/// This is useful if you need their data to set HTML document metadata or information that
/// needs to appear in HTTP headers.
#[track_caller]
pub fn new_with_options(
fut: impl Future<Output = T> + Send + 'static,
@@ -140,7 +125,7 @@ where
wakers,
suspenses,
ser: PhantomData,
#[cfg(any(debug_assertions, leptos_debuginfo))]
#[cfg(debug_assertions)]
defined_at: Location::caller(),
};
@@ -183,11 +168,11 @@ impl<T, Ser> ArcOnceResource<T, Ser> {
impl<T, Ser> DefinedAt for ArcOnceResource<T, Ser> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
#[cfg(not(debug_assertions))]
{
None
}
#[cfg(any(debug_assertions, leptos_debuginfo))]
#[cfg(debug_assertions)]
{
Some(self.defined_at)
}
@@ -253,8 +238,7 @@ where
}
}
/// A [`Future`] that is ready when an
/// [`ArcAsyncDerived`](reactive_graph::computed::ArcAsyncDerived) is finished loading or reloading,
/// A [`Future`] that is ready when an [`ArcAsyncDerived`] is finished loading or reloading,
/// and contains its value. `.await`ing this clones the value `T`.
pub struct OnceResourceFuture<T> {
source: AnySource,
@@ -272,7 +256,7 @@ where
#[track_caller]
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
#[cfg(any(debug_assertions, leptos_debuginfo))]
#[cfg(debug_assertions)]
let _guard = SpecialNonReactiveZone::enter();
let waker = cx.waker();
self.source.track();
@@ -303,17 +287,11 @@ where
<JsonSerdeCodec as Encoder<T>>::Encoded: IntoEncodedString,
<JsonSerdeCodec as Decoder<T>>::Encoded: FromEncodedStr,
{
/// Creates a resource using [`JsonSerdeCodec`] for encoding/decoding the value.
#[track_caller]
pub fn new(fut: impl Future<Output = T> + Send + 'static) -> Self {
ArcOnceResource::new_with_options(fut, false)
}
/// Creates a blocking resource using [`JsonSerdeCodec`] for encoding/decoding the value.
///
/// Blocking resources prevent any of the HTTP response from being sent until they have loaded.
/// This is useful if you need their data to set HTML document metadata or information that
/// needs to appear in HTTP headers.
#[track_caller]
pub fn new_blocking(fut: impl Future<Output = T> + Send + 'static) -> Self {
ArcOnceResource::new_with_options(fut, true)
@@ -329,7 +307,6 @@ T: Send + Sync + 'static,
<FromToStringCodec as Encoder<T>>::Encoded: IntoEncodedString,
<FromToStringCodec as Decoder<T>>::Encoded: FromEncodedStr,
{
/// Creates a resource using [`FromToStringCodec`] for encoding/decoding the value.
pub fn new_str(
fut: impl Future<Output = T> + Send + 'static
) -> Self
@@ -337,11 +314,6 @@ T: Send + Sync + 'static,
ArcOnceResource::new_with_options(fut, false)
}
/// Creates a blocking resource using [`FromToStringCodec`] for encoding/decoding the value.
///
/// Blocking resources prevent any of the HTTP response from being sent until they have loaded.
/// This is useful if you need their data to set HTML document metadata or information that
/// needs to appear in HTTP headers.
pub fn new_str_blocking(
fut: impl Future<Output = T> + Send + 'static
) -> Self
@@ -360,7 +332,6 @@ T: Send + Sync + 'static,
<JsonSerdeWasmCodec as Encoder<T>>::Encoded: IntoEncodedString,
<JsonSerdeWasmCodec as Decoder<T>>::Encoded: FromEncodedStr,
{
/// Creates a resource using [`JsonSerdeWasmCodec`] for encoding/decoding the value.
#[track_caller]
pub fn new_serde_wb(
fut: impl Future<Output = T> + Send + 'static
@@ -369,11 +340,6 @@ fut: impl Future<Output = T> + Send + 'static
ArcOnceResource::new_with_options(fut, false)
}
/// Creates a blocking resource using [`JsonSerdeWasmCodec`] for encoding/decoding the value.
///
/// Blocking resources prevent any of the HTTP response from being sent until they have loaded.
/// This is useful if you need their data to set HTML document metadata or information that
/// needs to appear in HTTP headers.
#[track_caller]
pub fn new_serde_wb_blocking(
fut: impl Future<Output = T> + Send + 'static
@@ -394,7 +360,6 @@ where
<MiniserdeCodec as Encoder<T>>::Encoded: IntoEncodedString,
<MiniserdeCodec as Decoder<T>>::Encoded: FromEncodedStr,
{
/// Creates a resource using [`MiniserdeCodec`] for encoding/decoding the value.
#[track_caller]
pub fn new_miniserde(
fut: impl Future<Output = T> + Send + 'static,
@@ -402,11 +367,6 @@ where
ArcOnceResource::new_with_options(fut, false)
}
/// Creates a blocking resource using [`MiniserdeCodec`] for encoding/decoding the value.
///
/// Blocking resources prevent any of the HTTP response from being sent until they have loaded.
/// This is useful if you need their data to set HTML document metadata or information that
/// needs to appear in HTTP headers.
#[track_caller]
pub fn new_miniserde_blocking(
fut: impl Future<Output = T> + Send + 'static,
@@ -425,7 +385,6 @@ T: Send + Sync + 'static,
<SerdeLite<JsonSerdeCodec> as Encoder<T>>::Encoded: IntoEncodedString,
<SerdeLite<JsonSerdeCodec> as Decoder<T>>::Encoded: FromEncodedStr,
{
/// Creates a resource using [`SerdeLite`] for encoding/decoding the value.
#[track_caller]
pub fn new_serde_lite(
fut: impl Future<Output = T> + Send + 'static
@@ -434,11 +393,6 @@ fut: impl Future<Output = T> + Send + 'static
ArcOnceResource::new_with_options(fut, false)
}
/// Creates a blocking resource using [`SerdeLite`] for encoding/decoding the value.
///
/// Blocking resources prevent any of the HTTP response from being sent until they have loaded.
/// This is useful if you need their data to set HTML document metadata or information that
/// needs to appear in HTTP headers.
#[track_caller]
pub fn new_serde_lite_blocking(
fut: impl Future<Output = T> + Send + 'static
@@ -460,17 +414,11 @@ where
<RkyvCodec as Encoder<T>>::Encoded: IntoEncodedString,
<RkyvCodec as Decoder<T>>::Encoded: FromEncodedStr,
{
/// Creates a resource using [`RkyvCodec`] for encoding/decoding the value.
#[track_caller]
pub fn new_rkyv(fut: impl Future<Output = T> + Send + 'static) -> Self {
ArcOnceResource::new_with_options(fut, false)
}
/// Creates a blocking resource using [`RkyvCodec`] for encoding/decoding the value.
///
/// Blocking resources prevent any of the HTTP response from being sent until they have loaded.
/// This is useful if you need their data to set HTML document metadata or information that
/// needs to appear in HTTP headers.
#[track_caller]
pub fn new_rkyv_blocking(
fut: impl Future<Output = T> + Send + 'static,
@@ -479,19 +427,10 @@ where
}
}
/// A resource that only loads once.
///
/// Resources allow asynchronously loading data and serializing it from the server to the client,
/// so that it loads on the server, and is then deserialized on the client. This improves
/// performance by beginning data loading on the server when the request is made, rather than
/// beginning it on the client after WASM has been loaded.
///
/// You can access the value of the resource either synchronously using `.get()` or asynchronously
/// using `.await`.
#[derive(Debug)]
pub struct OnceResource<T, Ser = JsonSerdeCodec> {
inner: ArenaItem<ArcOnceResource<T, Ser>>,
#[cfg(any(debug_assertions, leptos_debuginfo))]
#[cfg(debug_assertions)]
defined_at: &'static Location<'static>,
}
@@ -513,24 +452,18 @@ where
<Ser as Encoder<T>>::Encoded: IntoEncodedString,
<Ser as Decoder<T>>::Encoded: FromEncodedStr,
{
/// Creates a new resource with the encoding `Ser`. If `blocking` is `true`, this is a blocking
/// resource.
///
/// Blocking resources prevent any of the HTTP response from being sent until they have loaded.
/// This is useful if you need their data to set HTML document metadata or information that
/// needs to appear in HTTP headers.
#[track_caller]
pub fn new_with_options(
fut: impl Future<Output = T> + Send + 'static,
blocking: bool,
) -> Self {
#[cfg(any(debug_assertions, leptos_debuginfo))]
#[cfg(debug_assertions)]
let defined_at = Location::caller();
Self {
inner: ArenaItem::new(ArcOnceResource::new_with_options(
fut, blocking,
)),
#[cfg(any(debug_assertions, leptos_debuginfo))]
#[cfg(debug_assertions)]
defined_at,
}
}
@@ -551,11 +484,11 @@ where
impl<T, Ser> DefinedAt for OnceResource<T, Ser> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
#[cfg(not(debug_assertions))]
{
None
}
#[cfg(any(debug_assertions, leptos_debuginfo))]
#[cfg(debug_assertions)]
{
Some(self.defined_at)
}
@@ -634,17 +567,11 @@ where
<JsonSerdeCodec as Encoder<T>>::Encoded: IntoEncodedString,
<JsonSerdeCodec as Decoder<T>>::Encoded: FromEncodedStr,
{
/// Creates a resource using [`JsonSerdeCodec`] for encoding/decoding the value.
#[track_caller]
pub fn new(fut: impl Future<Output = T> + Send + 'static) -> Self {
OnceResource::new_with_options(fut, false)
}
/// Creates a blocking resource using [`JsonSerdeCodec`] for encoding/decoding the value.
///
/// Blocking resources prevent any of the HTTP response from being sent until they have loaded.
/// This is useful if you need their data to set HTML document metadata or information that
/// needs to appear in HTTP headers.
#[track_caller]
pub fn new_blocking(fut: impl Future<Output = T> + Send + 'static) -> Self {
OnceResource::new_with_options(fut, true)
@@ -660,7 +587,6 @@ T: Send + Sync + 'static,
<FromToStringCodec as Encoder<T>>::Encoded: IntoEncodedString,
<FromToStringCodec as Decoder<T>>::Encoded: FromEncodedStr,
{
/// Creates a resource using [`FromToStringCodec`] for encoding/decoding the value.
pub fn new_str(
fut: impl Future<Output = T> + Send + 'static
) -> Self
@@ -668,11 +594,6 @@ T: Send + Sync + 'static,
OnceResource::new_with_options(fut, false)
}
/// Creates a blocking resource using [`FromToStringCodec`] for encoding/decoding the value.
///
/// Blocking resources prevent any of the HTTP response from being sent until they have loaded.
/// This is useful if you need their data to set HTML document metadata or information that
/// needs to appear in HTTP headers.
pub fn new_str_blocking(
fut: impl Future<Output = T> + Send + 'static
) -> Self
@@ -691,7 +612,6 @@ T: Send + Sync + 'static,
<JsonSerdeWasmCodec as Encoder<T>>::Encoded: IntoEncodedString,
<JsonSerdeWasmCodec as Decoder<T>>::Encoded: FromEncodedStr,
{
/// Creates a resource using [`JsonSerdeWasmCodec`] for encoding/decoding the value.
#[track_caller]
pub fn new_serde_wb(
fut: impl Future<Output = T> + Send + 'static
@@ -700,11 +620,6 @@ fut: impl Future<Output = T> + Send + 'static
OnceResource::new_with_options(fut, false)
}
/// Creates a blocking resource using [`JsonSerdeWasmCodec`] for encoding/decoding the value.
///
/// Blocking resources prevent any of the HTTP response from being sent until they have loaded.
/// This is useful if you need their data to set HTML document metadata or information that
/// needs to appear in HTTP headers.
#[track_caller]
pub fn new_serde_wb_blocking(
fut: impl Future<Output = T> + Send + 'static
@@ -725,7 +640,6 @@ where
<MiniserdeCodec as Encoder<T>>::Encoded: IntoEncodedString,
<MiniserdeCodec as Decoder<T>>::Encoded: FromEncodedStr,
{
/// Creates a resource using [`MiniserdeCodec`] for encoding/decoding the value.
#[track_caller]
pub fn new_miniserde(
fut: impl Future<Output = T> + Send + 'static,
@@ -733,11 +647,6 @@ where
OnceResource::new_with_options(fut, false)
}
/// Creates a blocking resource using [`MiniserdeCodec`] for encoding/decoding the value.
///
/// Blocking resources prevent any of the HTTP response from being sent until they have loaded.
/// This is useful if you need their data to set HTML document metadata or information that
/// needs to appear in HTTP headers.
#[track_caller]
pub fn new_miniserde_blocking(
fut: impl Future<Output = T> + Send + 'static,
@@ -756,7 +665,6 @@ T: Send + Sync + 'static,
<SerdeLite<JsonSerdeCodec> as Encoder<T>>::Encoded: IntoEncodedString,
<SerdeLite<JsonSerdeCodec> as Decoder<T>>::Encoded: FromEncodedStr,
{
/// Creates a resource using [`SerdeLite`] for encoding/decoding the value.
#[track_caller]
pub fn new_serde_lite(
fut: impl Future<Output = T> + Send + 'static
@@ -765,11 +673,6 @@ fut: impl Future<Output = T> + Send + 'static
OnceResource::new_with_options(fut, false)
}
/// Creates a blocking resource using [`SerdeLite`] for encoding/decoding the value.
///
/// Blocking resources prevent any of the HTTP response from being sent until they have loaded.
/// This is useful if you need their data to set HTML document metadata or information that
/// needs to appear in HTTP headers.
#[track_caller]
pub fn new_serde_lite_blocking(
fut: impl Future<Output = T> + Send + 'static
@@ -791,17 +694,11 @@ where
<RkyvCodec as Encoder<T>>::Encoded: IntoEncodedString,
<RkyvCodec as Decoder<T>>::Encoded: FromEncodedStr,
{
/// Creates a resource using [`RkyvCodec`] for encoding/decoding the value.
#[track_caller]
pub fn new_rkyv(fut: impl Future<Output = T> + Send + 'static) -> Self {
OnceResource::new_with_options(fut, false)
}
/// Creates a blocking resource using [`RkyvCodec`] for encoding/decoding the value.
///
/// Blocking resources prevent any of the HTTP response from being sent until they have loaded.
/// This is useful if you need their data to set HTML document metadata or information that
/// needs to appear in HTTP headers.
#[track_caller]
pub fn new_rkyv_blocking(
fut: impl Future<Output = T> + Send + 'static,

View File

@@ -37,12 +37,9 @@ use std::{
pub(crate) static IS_SUPPRESSING_RESOURCE_LOAD: AtomicBool =
AtomicBool::new(false);
/// Used to prevent resources from actually loading, in environments (like server route generation)
/// where they are not needed.
pub struct SuppressResourceLoad;
impl SuppressResourceLoad {
/// Prevents resources from loading until this is dropped.
pub fn new() -> Self {
IS_SUPPRESSING_RESOURCE_LOAD.store(true, Ordering::Relaxed);
Self
@@ -61,20 +58,11 @@ impl Drop for SuppressResourceLoad {
}
}
/// A reference-counted asynchronous resource.
///
/// Resources allow asynchronously loading data and serializing it from the server to the client,
/// so that it loads on the server, and is then deserialized on the client. This improves
/// performance by beginning data loading on the server when the request is made, rather than
/// beginning it on the client after WASM has been loaded.
///
/// You can access the value of the resource either synchronously using `.get()` or asynchronously
/// using `.await`.
pub struct ArcResource<T, Ser = JsonSerdeCodec> {
ser: PhantomData<Ser>,
refetch: ArcRwSignal<usize>,
data: ArcAsyncDerived<T>,
#[cfg(any(debug_assertions, leptos_debuginfo))]
#[cfg(debug_assertions)]
defined_at: &'static Location<'static>,
}
@@ -82,7 +70,7 @@ impl<T, Ser> Debug for ArcResource<T, Ser> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut d = f.debug_struct("ArcResource");
d.field("ser", &self.ser).field("data", &self.data);
#[cfg(any(debug_assertions, leptos_debuginfo))]
#[cfg(debug_assertions)]
d.field("defined_at", self.defined_at);
d.finish_non_exhaustive()
}
@@ -98,7 +86,7 @@ where
ser: PhantomData,
data: arc_resource.data.into(),
refetch: arc_resource.refetch.into(),
#[cfg(any(debug_assertions, leptos_debuginfo))]
#[cfg(debug_assertions)]
defined_at: Location::caller(),
}
}
@@ -114,7 +102,7 @@ where
ser: PhantomData,
data: resource.data.into(),
refetch: resource.refetch.into(),
#[cfg(any(debug_assertions, leptos_debuginfo))]
#[cfg(debug_assertions)]
defined_at: Location::caller(),
}
}
@@ -122,11 +110,11 @@ where
impl<T, Ser> DefinedAt for ArcResource<T, Ser> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(any(debug_assertions, leptos_debuginfo))]
#[cfg(debug_assertions)]
{
Some(self.defined_at)
}
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
#[cfg(not(debug_assertions))]
{
None
}
@@ -139,7 +127,7 @@ impl<T, Ser> Clone for ArcResource<T, Ser> {
ser: self.ser,
refetch: self.refetch.clone(),
data: self.data.clone(),
#[cfg(any(debug_assertions, leptos_debuginfo))]
#[cfg(debug_assertions)]
defined_at: self.defined_at,
}
}
@@ -206,21 +194,6 @@ where
<Ser as Encoder<T>>::Encoded: IntoEncodedString,
<Ser as Decoder<T>>::Encoded: FromEncodedStr,
{
/// Creates a new resource with the encoding `Ser`.
///
/// This takes a `source` function and a `fetcher`. The resource memoizes and reactively tracks
/// the value returned by `source`. Whenever that value changes, it will run the `fetcher` to
/// generate a new [`Future`] to load data.
///
/// On creation, if you are on the server, this will run the `fetcher` once to generate
/// a `Future` whose value will be serialized from the server to the client. If you are on
/// the client, the initial value will be deserialized without re-running that async task.
///
/// If `blocking` is `true`, this is a blocking resource.
///
/// Blocking resources prevent any of the HTTP response from being sent until they have loaded.
/// This is useful if you need their data to set HTML document metadata or information that
/// needs to appear in HTTP headers.
#[track_caller]
pub fn new_with_options<S, Fut>(
source: impl Fn() -> S + Send + Sync + 'static,
@@ -300,13 +273,11 @@ where
ser: PhantomData,
data,
refetch,
#[cfg(any(debug_assertions, leptos_debuginfo))]
#[cfg(debug_assertions)]
defined_at: Location::caller(),
}
}
/// Synchronously, reactively reads the current value of the resource and applies the function
/// `f` to its value if it is `Some(_)`.
#[track_caller]
pub fn map<U>(&self, f: impl FnOnce(&T) -> U) -> Option<U>
where
@@ -380,13 +351,6 @@ where
T: Send + Sync + 'static,
E: Send + Sync + Clone + 'static,
{
/// Applies the given function when a resource that returns `Result<T, E>`
/// has resolved and loaded an `Ok(_)`, rather than requiring nested `.map()`
/// calls over the `Option<Result<_, _>>` returned by the resource.
///
/// This is useful when used with features like server functions, in conjunction
/// with `<ErrorBoundary/>` and `<Suspense/>`, when these other components are
/// left to handle the `None` and `Err(_)` states.
#[track_caller]
pub fn and_then<U>(&self, f: impl FnOnce(&T) -> U) -> Option<Result<U, E>> {
self.map(|data| data.as_ref().map(f).map_err(|e| e.clone()))
@@ -403,15 +367,6 @@ where
<JsonSerdeCodec as Encoder<T>>::Encoded: IntoEncodedString,
<JsonSerdeCodec as Decoder<T>>::Encoded: FromEncodedStr,
{
/// Creates a new resource with the encoding [`JsonSerdeCodec`].
///
/// This takes a `source` function and a `fetcher`. The resource memoizes and reactively tracks
/// the value returned by `source`. Whenever that value changes, it will run the `fetcher` to
/// generate a new [`Future`] to load data.
///
/// On creation, if you are on the server, this will run the `fetcher` once to generate
/// a `Future` whose value will be serialized from the server to the client. If you are on
/// the client, the initial value will be deserialized without re-running that async task.
#[track_caller]
pub fn new<S, Fut>(
source: impl Fn() -> S + Send + Sync + 'static,
@@ -425,19 +380,6 @@ where
ArcResource::new_with_options(source, fetcher, false)
}
/// Creates a new blocking resource with the encoding [`JsonSerdeCodec`].
///
/// This takes a `source` function and a `fetcher`. The resource memoizes and reactively tracks
/// the value returned by `source`. Whenever that value changes, it will run the `fetcher` to
/// generate a new [`Future`] to load data.
///
/// On creation, if you are on the server, this will run the `fetcher` once to generate
/// a `Future` whose value will be serialized from the server to the client. If you are on
/// the client, the initial value will be deserialized without re-running that async task.
///
/// Blocking resources prevent any of the HTTP response from being sent until they have loaded.
/// This is useful if you need their data to set HTML document metadata or information that
/// needs to appear in HTTP headers.
#[track_caller]
pub fn new_blocking<S, Fut>(
source: impl Fn() -> S + Send + Sync + 'static,
@@ -460,15 +402,6 @@ where
<FromToStringCodec as Encoder<T>>::Encoded: IntoEncodedString,
<FromToStringCodec as Decoder<T>>::Encoded: FromEncodedStr,
{
/// Creates a new resource with the encoding [`FromToStringCodec`].
///
/// This takes a `source` function and a `fetcher`. The resource memoizes and reactively tracks
/// the value returned by `source`. Whenever that value changes, it will run the `fetcher` to
/// generate a new [`Future`] to load data.
///
/// On creation, if you are on the server, this will run the `fetcher` once to generate
/// a `Future` whose value will be serialized from the server to the client. If you are on
/// the client, the initial value will be deserialized without re-running that async task.
pub fn new_str<S, Fut>(
source: impl Fn() -> S + Send + Sync + 'static,
fetcher: impl Fn(S) -> Fut + Send + Sync + 'static,
@@ -481,19 +414,6 @@ where
ArcResource::new_with_options(source, fetcher, false)
}
/// Creates a new blocking resource with the encoding [`FromToStringCodec`].
///
/// This takes a `source` function and a `fetcher`. The resource memoizes and reactively tracks
/// the value returned by `source`. Whenever that value changes, it will run the `fetcher` to
/// generate a new [`Future`] to load data.
///
/// On creation, if you are on the server, this will run the `fetcher` once to generate
/// a `Future` whose value will be serialized from the server to the client. If you are on
/// the client, the initial value will be deserialized without re-running that async task.
///
/// Blocking resources prevent any of the HTTP response from being sent until they have loaded.
/// This is useful if you need their data to set HTML document metadata or information that
/// needs to appear in HTTP headers.
pub fn new_str_blocking<S, Fut>(
source: impl Fn() -> S + Send + Sync + 'static,
fetcher: impl Fn(S) -> Fut + Send + Sync + 'static,
@@ -516,15 +436,6 @@ where
<JsonSerdeWasmCodec as Encoder<T>>::Encoded: IntoEncodedString,
<JsonSerdeWasmCodec as Decoder<T>>::Encoded: FromEncodedStr,
{
/// Creates a new resource with the encoding [`JsonSerdeWasmCodec`].
///
/// This takes a `source` function and a `fetcher`. The resource memoizes and reactively tracks
/// the value returned by `source`. Whenever that value changes, it will run the `fetcher` to
/// generate a new [`Future`] to load data.
///
/// On creation, if you are on the server, this will run the `fetcher` once to generate
/// a `Future` whose value will be serialized from the server to the client. If you are on
/// the client, the initial value will be deserialized without re-running that async task.
#[track_caller]
pub fn new_serde_wb<S, Fut>(
source: impl Fn() -> S + Send + Sync + 'static,
@@ -538,19 +449,6 @@ where
ArcResource::new_with_options(source, fetcher, false)
}
/// Creates a new blocking resource with the encoding [`JsonSerdeWasmCodec`].
///
/// This takes a `source` function and a `fetcher`. The resource memoizes and reactively tracks
/// the value returned by `source`. Whenever that value changes, it will run the `fetcher` to
/// generate a new [`Future`] to load data.
///
/// On creation, if you are on the server, this will run the `fetcher` once to generate
/// a `Future` whose value will be serialized from the server to the client. If you are on
/// the client, the initial value will be deserialized without re-running that async task.
///
/// Blocking resources prevent any of the HTTP response from being sent until they have loaded.
/// This is useful if you need their data to set HTML document metadata or information that
/// needs to appear in HTTP headers.
#[track_caller]
pub fn new_serde_wb_blocking<S, Fut>(
source: impl Fn() -> S + Send + Sync + 'static,
@@ -575,15 +473,6 @@ where
<MiniserdeCodec as Encoder<T>>::Encoded: IntoEncodedString,
<MiniserdeCodec as Decoder<T>>::Encoded: FromEncodedStr,
{
/// Creates a new resource with the encoding [`MiniserdeCodec`].
///
/// This takes a `source` function and a `fetcher`. The resource memoizes and reactively tracks
/// the value returned by `source`. Whenever that value changes, it will run the `fetcher` to
/// generate a new [`Future`] to load data.
///
/// On creation, if you are on the server, this will run the `fetcher` once to generate
/// a `Future` whose value will be serialized from the server to the client. If you are on
/// the client, the initial value will be deserialized without re-running that async task.
#[track_caller]
pub fn new_miniserde<S, Fut>(
source: impl Fn() -> S + Send + Sync + 'static,
@@ -597,19 +486,6 @@ where
ArcResource::new_with_options(source, fetcher, false)
}
/// Creates a new blocking resource with the encoding [`MiniserdeCodec`].
///
/// This takes a `source` function and a `fetcher`. The resource memoizes and reactively tracks
/// the value returned by `source`. Whenever that value changes, it will run the `fetcher` to
/// generate a new [`Future`] to load data.
///
/// On creation, if you are on the server, this will run the `fetcher` once to generate
/// a `Future` whose value will be serialized from the server to the client. If you are on
/// the client, the initial value will be deserialized without re-running that async task.
///
/// Blocking resources prevent any of the HTTP response from being sent until they have loaded.
/// This is useful if you need their data to set HTML document metadata or information that
/// needs to appear in HTTP headers.
#[track_caller]
pub fn new_miniserde_blocking<S, Fut>(
source: impl Fn() -> S + Send + Sync + 'static,
@@ -633,15 +509,6 @@ where
<SerdeLite<JsonSerdeCodec> as Encoder<T>>::Encoded: IntoEncodedString,
<SerdeLite<JsonSerdeCodec> as Decoder<T>>::Encoded: FromEncodedStr,
{
/// Creates a new resource with the encoding [`SerdeLite`].
///
/// This takes a `source` function and a `fetcher`. The resource memoizes and reactively tracks
/// the value returned by `source`. Whenever that value changes, it will run the `fetcher` to
/// generate a new [`Future`] to load data.
///
/// On creation, if you are on the server, this will run the `fetcher` once to generate
/// a `Future` whose value will be serialized from the server to the client. If you are on
/// the client, the initial value will be deserialized without re-running that async task.
#[track_caller]
pub fn new_serde_lite<S, Fut>(
source: impl Fn() -> S + Send + Sync + 'static,
@@ -655,19 +522,6 @@ where
ArcResource::new_with_options(source, fetcher, false)
}
/// Creates a new blocking resource with the encoding [`SerdeLite`].
///
/// This takes a `source` function and a `fetcher`. The resource memoizes and reactively tracks
/// the value returned by `source`. Whenever that value changes, it will run the `fetcher` to
/// generate a new [`Future`] to load data.
///
/// On creation, if you are on the server, this will run the `fetcher` once to generate
/// a `Future` whose value will be serialized from the server to the client. If you are on
/// the client, the initial value will be deserialized without re-running that async task.
///
/// Blocking resources prevent any of the HTTP response from being sent until they have loaded.
/// This is useful if you need their data to set HTML document metadata or information that
/// needs to appear in HTTP headers.
#[track_caller]
pub fn new_serde_lite_blocking<S, Fut>(
source: impl Fn() -> S + Send + Sync + 'static,
@@ -693,15 +547,6 @@ where
<RkyvCodec as Encoder<T>>::Encoded: IntoEncodedString,
<RkyvCodec as Decoder<T>>::Encoded: FromEncodedStr,
{
/// Creates a new resource with the encoding [`RkyvCodec`].
///
/// This takes a `source` function and a `fetcher`. The resource memoizes and reactively tracks
/// the value returned by `source`. Whenever that value changes, it will run the `fetcher` to
/// generate a new [`Future`] to load data.
///
/// On creation, if you are on the server, this will run the `fetcher` once to generate
/// a `Future` whose value will be serialized from the server to the client. If you are on
/// the client, the initial value will be deserialized without re-running that async task.
#[track_caller]
pub fn new_rkyv<S, Fut>(
source: impl Fn() -> S + Send + Sync + 'static,
@@ -715,19 +560,6 @@ where
ArcResource::new_with_options(source, fetcher, false)
}
/// Creates a new blocking resource with the encoding [`RkyvCodec`].
///
/// This takes a `source` function and a `fetcher`. The resource memoizes and reactively tracks
/// the value returned by `source`. Whenever that value changes, it will run the `fetcher` to
/// generate a new [`Future`] to load data.
///
/// On creation, if you are on the server, this will run the `fetcher` once to generate
/// a `Future` whose value will be serialized from the server to the client. If you are on
/// the client, the initial value will be deserialized without re-running that async task.
///
/// Blocking resources prevent any of the HTTP response from being sent until they have loaded.
/// This is useful if you need their data to set HTML document metadata or information that
/// needs to appear in HTTP headers.
#[track_caller]
pub fn new_rkyv_blocking<S, Fut>(
source: impl Fn() -> S + Send + Sync + 'static,
@@ -758,22 +590,11 @@ impl<T, Ser> ArcResource<T, Ser>
where
T: 'static,
{
/// Returns a new [`Future`] that is ready when the resource has loaded, and accesses its inner
/// value by reference.
pub fn by_ref(&self) -> AsyncDerivedRefFuture<T> {
self.data.by_ref()
}
}
/// An asynchronous resource.
///
/// Resources allow asynchronously loading data and serializing it from the server to the client,
/// so that it loads on the server, and is then deserialized on the client. This improves
/// performance by beginning data loading on the server when the request is made, rather than
/// beginning it on the client after WASM has been loaded.
///
/// You can access the value of the resource either synchronously using `.get()` or asynchronously
/// using `.await`.
pub struct Resource<T, Ser = JsonSerdeCodec>
where
T: Send + Sync + 'static,
@@ -781,7 +602,7 @@ where
ser: PhantomData<Ser>,
data: AsyncDerived<T>,
refetch: RwSignal<usize>,
#[cfg(any(debug_assertions, leptos_debuginfo))]
#[cfg(debug_assertions)]
defined_at: &'static Location<'static>,
}
@@ -792,7 +613,7 @@ where
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut d = f.debug_struct("ArcResource");
d.field("ser", &self.ser).field("data", &self.data);
#[cfg(any(debug_assertions, leptos_debuginfo))]
#[cfg(debug_assertions)]
d.field("defined_at", self.defined_at);
d.finish_non_exhaustive()
}
@@ -803,11 +624,11 @@ where
T: Send + Sync + 'static,
{
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(any(debug_assertions, leptos_debuginfo))]
#[cfg(debug_assertions)]
{
Some(self.defined_at)
}
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
#[cfg(not(debug_assertions))]
{
None
}
@@ -886,15 +707,6 @@ where
<FromToStringCodec as Decoder<T>>::Encoded: FromEncodedStr,
T: Send + Sync,
{
/// Creates a new resource with the encoding [`FromToStringCodec`].
///
/// This takes a `source` function and a `fetcher`. The resource memoizes and reactively tracks
/// the value returned by `source`. Whenever that value changes, it will run the `fetcher` to
/// generate a new [`Future`] to load data.
///
/// On creation, if you are on the server, this will run the `fetcher` once to generate
/// a `Future` whose value will be serialized from the server to the client. If you are on
/// the client, the initial value will be deserialized without re-running that async task.
#[track_caller]
pub fn new_str<S, Fut>(
source: impl Fn() -> S + Send + Sync + 'static,
@@ -908,19 +720,6 @@ where
Resource::new_with_options(source, fetcher, false)
}
/// Creates a new blocking resource with the encoding [`FromToStringCodec`].
///
/// This takes a `source` function and a `fetcher`. The resource memoizes and reactively tracks
/// the value returned by `source`. Whenever that value changes, it will run the `fetcher` to
/// generate a new [`Future`] to load data.
///
/// On creation, if you are on the server, this will run the `fetcher` once to generate
/// a `Future` whose value will be serialized from the server to the client. If you are on
/// the client, the initial value will be deserialized without re-running that async task.
///
/// Blocking resources prevent any of the HTTP response from being sent until they have loaded.
/// This is useful if you need their data to set HTML document metadata or information that
/// needs to appear in HTTP headers.
#[track_caller]
pub fn new_str_blocking<S, Fut>(
source: impl Fn() -> S + Send + Sync + 'static,
@@ -946,15 +745,6 @@ where
<JsonSerdeCodec as Decoder<T>>::Encoded: FromEncodedStr,
T: Send + Sync,
{
/// Creates a new resource with the encoding [`JsonSerdeCodec`].
///
/// This takes a `source` function and a `fetcher`. The resource memoizes and reactively tracks
/// the value returned by `source`. Whenever that value changes, it will run the `fetcher` to
/// generate a new [`Future`] to load data.
///
/// On creation, if you are on the server, this will run the `fetcher` once to generate
/// a `Future` whose value will be serialized from the server to the client. If you are on
/// the client, the initial value will be deserialized without re-running that async task.
#[track_caller]
pub fn new<S, Fut>(
source: impl Fn() -> S + Send + Sync + 'static,
@@ -968,19 +758,6 @@ where
Resource::new_with_options(source, fetcher, false)
}
/// Creates a new blocking resource with the encoding [`JsonSerdeCodec`].
///
/// This takes a `source` function and a `fetcher`. The resource memoizes and reactively tracks
/// the value returned by `source`. Whenever that value changes, it will run the `fetcher` to
/// generate a new [`Future`] to load data.
///
/// On creation, if you are on the server, this will run the `fetcher` once to generate
/// a `Future` whose value will be serialized from the server to the client. If you are on
/// the client, the initial value will be deserialized without re-running that async task.
///
/// Blocking resources prevent any of the HTTP response from being sent until they have loaded.
/// This is useful if you need their data to set HTML document metadata or information that
/// needs to appear in HTTP headers.
#[track_caller]
pub fn new_blocking<S, Fut>(
source: impl Fn() -> S + Send + Sync + 'static,
@@ -1005,15 +782,6 @@ where
<JsonSerdeWasmCodec as Decoder<T>>::Encoded: FromEncodedStr,
T: Send + Sync,
{
/// Creates a new resource with the encoding [`JsonSerdeWasmCodec`].
///
/// This takes a `source` function and a `fetcher`. The resource memoizes and reactively tracks
/// the value returned by `source`. Whenever that value changes, it will run the `fetcher` to
/// generate a new [`Future`] to load data.
///
/// On creation, if you are on the server, this will run the `fetcher` once to generate
/// a `Future` whose value will be serialized from the server to the client. If you are on
/// the client, the initial value will be deserialized without re-running that async task.
pub fn new_serde_wb<S, Fut>(
source: impl Fn() -> S + Send + Sync + 'static,
fetcher: impl Fn(S) -> Fut + Send + Sync + 'static,
@@ -1026,19 +794,6 @@ where
Resource::new_with_options(source, fetcher, false)
}
/// Creates a new blocking resource with the encoding [`JsonSerdeWasmCodec`].
///
/// This takes a `source` function and a `fetcher`. The resource memoizes and reactively tracks
/// the value returned by `source`. Whenever that value changes, it will run the `fetcher` to
/// generate a new [`Future`] to load data.
///
/// On creation, if you are on the server, this will run the `fetcher` once to generate
/// a `Future` whose value will be serialized from the server to the client. If you are on
/// the client, the initial value will be deserialized without re-running that async task.
///
/// Blocking resources prevent any of the HTTP response from being sent until they have loaded.
/// This is useful if you need their data to set HTML document metadata or information that
/// needs to appear in HTTP headers.
pub fn new_serde_wb_blocking<S, Fut>(
source: impl Fn() -> S + Send + Sync + 'static,
fetcher: impl Fn(S) -> Fut + Send + Sync + 'static,
@@ -1064,15 +819,6 @@ where
<MiniserdeCodec as Decoder<T>>::Encoded: FromEncodedStr,
T: Send + Sync,
{
/// Creates a new resource with the encoding [`MiniserdeCodec`].
///
/// This takes a `source` function and a `fetcher`. The resource memoizes and reactively tracks
/// the value returned by `source`. Whenever that value changes, it will run the `fetcher` to
/// generate a new [`Future`] to load data.
///
/// On creation, if you are on the server, this will run the `fetcher` once to generate
/// a `Future` whose value will be serialized from the server to the client. If you are on
/// the client, the initial value will be deserialized without re-running that async task.
pub fn new_miniserde<S, Fut>(
source: impl Fn() -> S + Send + Sync + 'static,
fetcher: impl Fn(S) -> Fut + Send + Sync + 'static,
@@ -1084,31 +830,6 @@ where
{
Resource::new_with_options(source, fetcher, false)
}
/// Creates a new blocking resource with the encoding [`MiniserdeCodec`].
///
/// This takes a `source` function and a `fetcher`. The resource memoizes and reactively tracks
/// the value returned by `source`. Whenever that value changes, it will run the `fetcher` to
/// generate a new [`Future`] to load data.
///
/// On creation, if you are on the server, this will run the `fetcher` once to generate
/// a `Future` whose value will be serialized from the server to the client. If you are on
/// the client, the initial value will be deserialized without re-running that async task.
///
/// Blocking resources prevent any of the HTTP response from being sent until they have loaded.
/// This is useful if you need their data to set HTML document metadata or information that
/// needs to appear in HTTP headers.
pub fn new_miniserde_blocking<S, Fut>(
source: impl Fn() -> S + Send + Sync + 'static,
fetcher: impl Fn(S) -> Fut + Send + Sync + 'static,
) -> Self
where
S: PartialEq + Clone + Send + Sync + 'static,
T: Send + Sync + 'static,
Fut: Future<Output = T> + Send + 'static,
{
Resource::new_with_options(source, fetcher, true)
}
}
#[cfg(feature = "serde-lite")]
@@ -1122,15 +843,6 @@ where
<SerdeLite<JsonSerdeCodec> as Decoder<T>>::Encoded: FromEncodedStr,
T: Send + Sync,
{
/// Creates a new resource with the encoding [`SerdeLite`].
///
/// This takes a `source` function and a `fetcher`. The resource memoizes and reactively tracks
/// the value returned by `source`. Whenever that value changes, it will run the `fetcher` to
/// generate a new [`Future`] to load data.
///
/// On creation, if you are on the server, this will run the `fetcher` once to generate
/// a `Future` whose value will be serialized from the server to the client. If you are on
/// the client, the initial value will be deserialized without re-running that async task.
pub fn new_serde_lite<S, Fut>(
source: impl Fn() -> S + Send + Sync + 'static,
fetcher: impl Fn(S) -> Fut + Send + Sync + 'static,
@@ -1143,19 +855,6 @@ where
Resource::new_with_options(source, fetcher, false)
}
/// Creates a new blocking resource with the encoding [`SerdeLite`].
///
/// This takes a `source` function and a `fetcher`. The resource memoizes and reactively tracks
/// the value returned by `source`. Whenever that value changes, it will run the `fetcher` to
/// generate a new [`Future`] to load data.
///
/// On creation, if you are on the server, this will run the `fetcher` once to generate
/// a `Future` whose value will be serialized from the server to the client. If you are on
/// the client, the initial value will be deserialized without re-running that async task.
///
/// Blocking resources prevent any of the HTTP response from being sent until they have loaded.
/// This is useful if you need their data to set HTML document metadata or information that
/// needs to appear in HTTP headers.
pub fn new_serde_lite_blocking<S, Fut>(
source: impl Fn() -> S + Send + Sync + 'static,
fetcher: impl Fn(S) -> Fut + Send + Sync + 'static,
@@ -1181,15 +880,6 @@ where
<RkyvCodec as Decoder<T>>::Encoded: FromEncodedStr,
T: Send + Sync,
{
/// Creates a new resource with the encoding [`RkyvCodec`].
///
/// This takes a `source` function and a `fetcher`. The resource memoizes and reactively tracks
/// the value returned by `source`. Whenever that value changes, it will run the `fetcher` to
/// generate a new [`Future`] to load data.
///
/// On creation, if you are on the server, this will run the `fetcher` once to generate
/// a `Future` whose value will be serialized from the server to the client. If you are on
/// the client, the initial value will be deserialized without re-running that async task.
pub fn new_rkyv<S, Fut>(
source: impl Fn() -> S + Send + Sync + 'static,
fetcher: impl Fn(S) -> Fut + Send + Sync + 'static,
@@ -1202,19 +892,6 @@ where
Resource::new_with_options(source, fetcher, false)
}
/// Creates a new blocking resource with the encoding [`RkyvCodec`].
///
/// This takes a `source` function and a `fetcher`. The resource memoizes and reactively tracks
/// the value returned by `source`. Whenever that value changes, it will run the `fetcher` to
/// generate a new [`Future`] to load data.
///
/// On creation, if you are on the server, this will run the `fetcher` once to generate
/// a `Future` whose value will be serialized from the server to the client. If you are on
/// the client, the initial value will be deserialized without re-running that async task.
///
/// Blocking resources prevent any of the HTTP response from being sent until they have loaded.
/// This is useful if you need their data to set HTML document metadata or information that
/// needs to appear in HTTP headers.
pub fn new_rkyv_blocking<S, Fut>(
source: impl Fn() -> S + Send + Sync + 'static,
fetcher: impl Fn(S) -> Fut + Send + Sync + 'static,
@@ -1238,21 +915,6 @@ where
<Ser as Decoder<T>>::Encoded: FromEncodedStr,
T: Send + Sync,
{
/// Creates a new resource with the encoding `Ser`.
///
/// This takes a `source` function and a `fetcher`. The resource memoizes and reactively tracks
/// the value returned by `source`. Whenever that value changes, it will run the `fetcher` to
/// generate a new [`Future`] to load data.
///
/// On creation, if you are on the server, this will run the `fetcher` once to generate
/// a `Future` whose value will be serialized from the server to the client. If you are on
/// the client, the initial value will be deserialized without re-running that async task.
///
/// If `blocking` is `true`, this is a blocking resource.
///
/// Blocking resources prevent any of the HTTP response from being sent until they have loaded.
/// This is useful if you need their data to set HTML document metadata or information that
/// needs to appear in HTTP headers.
#[track_caller]
pub fn new_with_options<S, Fut>(
source: impl Fn() -> S + Send + Sync + 'static,
@@ -1270,13 +932,11 @@ where
ser: PhantomData,
data: data.into(),
refetch: refetch.into(),
#[cfg(any(debug_assertions, leptos_debuginfo))]
#[cfg(debug_assertions)]
defined_at: Location::caller(),
}
}
/// Synchronously, reactively reads the current value of the resource and applies the function
/// `f` to its value if it is `Some(_)`.
pub fn map<U>(&self, f: impl FnOnce(&T) -> U) -> Option<U> {
self.data
.try_with(|n| n.as_ref().map(|n| Some(f(n))))?
@@ -1301,13 +961,6 @@ where
T: Send + Sync,
E: Send + Sync + Clone,
{
/// Applies the given function when a resource that returns `Result<T, E>`
/// has resolved and loaded an `Ok(_)`, rather than requiring nested `.map()`
/// calls over the `Option<Result<_, _>>` returned by the resource.
///
/// This is useful when used with features like server functions, in conjunction
/// with `<ErrorBoundary/>` and `<Suspense/>`, when these other components are
/// left to handle the `None` and `Err(_)` states.
#[track_caller]
pub fn and_then<U>(&self, f: impl FnOnce(&T) -> U) -> Option<Result<U, E>> {
self.map(|data| data.as_ref().map(f).map_err(|e| e.clone()))
@@ -1331,8 +984,6 @@ impl<T, Ser> Resource<T, Ser>
where
T: Send + Sync + 'static,
{
/// Returns a new [`Future`] that is ready when the resource has loaded, and accesses its inner
/// value by reference.
pub fn by_ref(&self) -> AsyncDerivedRefFuture<T> {
self.data.by_ref()
}

View File

@@ -31,7 +31,6 @@ pub struct SharedValue<T, Ser = JsonSerdeCodec> {
}
impl<T, Ser> SharedValue<T, Ser> {
/// Returns the inner value.
pub fn into_inner(self) -> T {
self.value
}
@@ -47,12 +46,6 @@ where
<<JsonSerdeCodec as codee::Decoder<T>>::Encoded as FromEncodedStr>::DecodingError:
Debug,
{
/// Wraps the initial value.
///
/// If this is on the server, the function will be invoked and the value serialized. When it runs
/// on the client, it will be deserialized without running the function again.
///
/// This uses the [`JsonSerdeCodec`] encoding.
pub fn new(initial: impl FnOnce() -> T) -> Self {
SharedValue::new_with_encoding(initial)
}
@@ -68,12 +61,6 @@ where
<<FromToStringCodec as codee::Decoder<T>>::Encoded as FromEncodedStr>::DecodingError:
Debug,
{
/// Wraps the initial value.
///
/// If this is on the server, the function will be invoked and the value serialized. When it runs
/// on the client, it will be deserialized without running the function again.
///
/// This uses the [`FromToStringCodec`] encoding.
pub fn new_str(initial: impl FnOnce() -> T) -> Self {
SharedValue::new_with_encoding(initial)
}
@@ -90,13 +77,7 @@ where
<<SerdeLite<JsonSerdeCodec> as codee::Decoder<T>>::Encoded as FromEncodedStr>::DecodingError:
Debug,
{
/// Wraps the initial value.
///
/// If this is on the server, the function will be invoked and the value serialized. When it runs
/// on the client, it will be deserialized without running the function again.
///
/// This uses the [`SerdeLite`] encoding.
pub fn new_serde_lite(initial: impl FnOnce() -> T) -> Self {
pub fn new(initial: impl FnOnce() -> T) -> Self {
SharedValue::new_with_encoding(initial)
}
}
@@ -112,13 +93,7 @@ where
<<JsonSerdeWasmCodec as codee::Decoder<T>>::Encoded as FromEncodedStr>::DecodingError:
Debug,
{
/// Wraps the initial value.
///
/// If this is on the server, the function will be invoked and the value serialized. When it runs
/// on the client, it will be deserialized without running the function again.
///
/// This uses the [`JsonSerdeWasmCodec`] encoding.
pub fn new_serde_wb(initial: impl FnOnce() -> T) -> Self {
pub fn new(initial: impl FnOnce() -> T) -> Self {
SharedValue::new_with_encoding(initial)
}
}
@@ -134,13 +109,7 @@ where
<<MiniserdeCodec as codee::Decoder<T>>::Encoded as FromEncodedStr>::DecodingError:
Debug,
{
/// Wraps the initial value.
///
/// If this is on the server, the function will be invoked and the value serialized. When it runs
/// on the client, it will be deserialized without running the function again.
///
/// This uses the [`MiniserdeCodec`] encoding.
pub fn new_miniserde(initial: impl FnOnce() -> T) -> Self {
pub fn new(initial: impl FnOnce() -> T) -> Self {
SharedValue::new_with_encoding(initial)
}
}
@@ -156,13 +125,7 @@ where
<<RkyvCodec as codee::Decoder<T>>::Encoded as FromEncodedStr>::DecodingError:
Debug,
{
/// Wraps the initial value.
///
/// If this is on the server, the function will be invoked and the value serialized. When it runs
/// on the client, it will be deserialized without running the function again.
///
/// This uses the [`RkyvCodec`] encoding.
pub fn new_rkyv(initial: impl FnOnce() -> T) -> Self {
pub fn new(initial: impl FnOnce() -> T) -> Self {
SharedValue::new_with_encoding(initial)
}
}
@@ -177,12 +140,6 @@ where
<<Ser as codee::Decoder<T>>::Encoded as FromEncodedStr>::DecodingError:
Debug,
{
/// Wraps the initial value.
///
/// If this is on the server, the function will be invoked and the value serialized. When it runs
/// on the client, it will be deserialized without running the function again.
///
/// This uses `Ser` as an encoding.
pub fn new_with_encoding(initial: impl FnOnce() -> T) -> Self {
let value: T;
#[cfg(feature = "hydration")]

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_meta"
version = "0.8.0-alpha"
version = "0.7.0-rc1"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
@@ -14,8 +14,8 @@ once_cell = "1.20"
or_poisoned = { workspace = true }
indexmap = "2.6"
send_wrapper = "0.6.0"
tracing = { version = "0.1.41", optional = true }
wasm-bindgen = "0.2.97"
tracing = { version = "0.1.40", optional = true }
wasm-bindgen = "0.2.95"
futures = "0.3.31"
[dependencies.web-sys]
@@ -26,13 +26,9 @@ features = ["HtmlLinkElement", "HtmlMetaElement", "HtmlTitleElement"]
default = []
ssr = []
tracing = ["dep:tracing"]
nonce = ["leptos/nonce"]
[package.metadata.docs.rs]
rustdoc-args = ["--generate-link-to-definition"]
[package.metadata.cargo-all-features]
denylist = ["tracing"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }

View File

@@ -1,9 +1,6 @@
use crate::ServerMetaContext;
use leptos::{
attr::{
any_attribute::{AnyAttribute, AnyAttributeState},
NextAttribute,
},
attr::NextAttribute,
component, html,
reactive::owner::use_context,
tachys::{
@@ -11,8 +8,8 @@ use leptos::{
html::attribute::Attribute,
hydration::Cursor,
view::{
add_attr::AddAnyAttr, any_view::ExtraAttrsMut, Mountable, Position,
PositionState, Render, RenderHtml,
add_attr::AddAnyAttr, Mountable, Position, PositionState, Render,
RenderHtml,
},
},
IntoView,
@@ -61,7 +58,6 @@ where
At: Attribute,
{
attributes: At::State,
extra_attrs: Option<Vec<AnyAttributeState>>,
}
impl<At> Render for BodyView<At>
@@ -70,27 +66,15 @@ where
{
type State = BodyViewState<At>;
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
fn build(self) -> Self::State {
let el = document().body().expect("there to be a <body> element");
let attributes = self.attributes.build(&el);
let extra_attrs = extra_attrs.map(|attrs| attrs.build(&el));
BodyViewState {
attributes,
extra_attrs,
}
BodyViewState { attributes }
}
fn rebuild(
self,
state: &mut Self::State,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
fn rebuild(self, state: &mut Self::State) {
self.attributes.rebuild(&mut state.attributes);
if let (Some(extra_attrs), Some(extra_attr_states)) =
(extra_attrs, &mut state.extra_attrs)
{
extra_attrs.rebuild(extra_attr_states);
}
}
}
@@ -119,24 +103,17 @@ where
At: Attribute,
{
type AsyncOutput = BodyView<At::AsyncOutput>;
type Owned = BodyView<At::CloneableOwned>;
const MIN_LENGTH: usize = At::MIN_LENGTH;
fn dry_resolve(&mut self, mut extra_attrs: ExtraAttrsMut<'_>) {
fn dry_resolve(&mut self) {
self.attributes.dry_resolve();
extra_attrs.iter_mut().for_each(Attribute::dry_resolve);
}
async fn resolve(
self,
extra_attrs: ExtraAttrsMut<'_>,
) -> Self::AsyncOutput {
let (attributes, _) = futures::join!(
self.attributes.resolve(),
ExtraAttrsMut::resolve(extra_attrs)
);
BodyView { attributes }
async fn resolve(self) -> Self::AsyncOutput {
BodyView {
attributes: self.attributes.resolve().await,
}
}
fn to_html_with_buf(
@@ -145,15 +122,10 @@ where
_position: &mut Position,
_escape: bool,
_mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
if let Some(meta) = use_context::<ServerMetaContext>() {
let mut buf = String::new();
_ = html::attributes_to_html(
self.attributes,
extra_attrs,
&mut buf,
);
_ = html::attributes_to_html(self.attributes, &mut buf);
if !buf.is_empty() {
_ = meta.body.send(buf);
}
@@ -164,23 +136,11 @@ where
self,
_cursor: &Cursor,
_position: &PositionState,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
let el = document().body().expect("there to be a <body> element");
let attributes = self.attributes.hydrate::<FROM_SERVER>(&el);
let extra_attrs =
extra_attrs.map(|attrs| attrs.hydrate::<FROM_SERVER>(&el));
BodyViewState {
attributes,
extra_attrs,
}
}
fn into_owned(self) -> Self::Owned {
BodyView {
attributes: self.attributes.into_cloneable_owned(),
}
BodyViewState { attributes }
}
}

View File

@@ -1,9 +1,6 @@
use crate::ServerMetaContext;
use leptos::{
attr::{
any_attribute::{AnyAttribute, AnyAttributeState},
NextAttribute,
},
attr::NextAttribute,
component, html,
reactive::owner::use_context,
tachys::{
@@ -11,8 +8,8 @@ use leptos::{
html::attribute::Attribute,
hydration::Cursor,
view::{
add_attr::AddAnyAttr, any_view::ExtraAttrsMut, Mountable, Position,
PositionState, Render, RenderHtml,
add_attr::AddAnyAttr, Mountable, Position, PositionState, Render,
RenderHtml,
},
},
IntoView,
@@ -58,7 +55,6 @@ where
At: Attribute,
{
attributes: At::State,
extra_attrs: Option<Vec<AnyAttributeState>>,
}
impl<At> Render for HtmlView<At>
@@ -67,33 +63,18 @@ where
{
type State = HtmlViewState<At>;
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
fn build(self) -> Self::State {
let el = document()
.document_element()
.expect("there to be a <html> element");
let attributes = self.attributes.build(&el);
let extra_attrs = extra_attrs.map(|attrs| {
attrs.into_iter().map(|attr| attr.build(&el)).collect()
});
HtmlViewState {
attributes,
extra_attrs,
}
HtmlViewState { attributes }
}
fn rebuild(
self,
state: &mut Self::State,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
fn rebuild(self, state: &mut Self::State) {
self.attributes.rebuild(&mut state.attributes);
if let (Some(extra_attrs), Some(extra_attr_states)) =
(extra_attrs, &mut state.extra_attrs)
{
extra_attrs.rebuild(extra_attr_states);
}
}
}
@@ -122,24 +103,17 @@ where
At: Attribute,
{
type AsyncOutput = HtmlView<At::AsyncOutput>;
type Owned = HtmlView<At::CloneableOwned>;
const MIN_LENGTH: usize = At::MIN_LENGTH;
fn dry_resolve(&mut self, mut extra_attrs: ExtraAttrsMut<'_>) {
fn dry_resolve(&mut self) {
self.attributes.dry_resolve();
extra_attrs.iter_mut().for_each(Attribute::dry_resolve);
}
async fn resolve(
self,
extra_attrs: ExtraAttrsMut<'_>,
) -> Self::AsyncOutput {
let (attributes, _) = futures::join!(
self.attributes.resolve(),
ExtraAttrsMut::resolve(extra_attrs)
);
HtmlView { attributes }
async fn resolve(self) -> Self::AsyncOutput {
HtmlView {
attributes: self.attributes.resolve().await,
}
}
fn to_html_with_buf(
@@ -148,15 +122,10 @@ where
_position: &mut Position,
_escape: bool,
_mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
if let Some(meta) = use_context::<ServerMetaContext>() {
let mut buf = String::new();
_ = html::attributes_to_html(
self.attributes,
extra_attrs,
&mut buf,
);
_ = html::attributes_to_html(self.attributes, &mut buf);
if !buf.is_empty() {
_ = meta.html.send(buf);
}
@@ -167,30 +136,14 @@ where
self,
_cursor: &Cursor,
_position: &PositionState,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
let el = document()
.document_element()
.expect("there to be a <html> element");
let attributes = self.attributes.hydrate::<FROM_SERVER>(&el);
let extra_attrs = extra_attrs.map(|attrs| {
attrs
.into_iter()
.map(|attr| attr.hydrate::<FROM_SERVER>(&el))
.collect()
});
HtmlViewState {
attributes,
extra_attrs,
}
}
fn into_owned(self) -> Self::Owned {
HtmlView {
attributes: self.attributes.into_cloneable_owned(),
}
HtmlViewState { attributes }
}
}

View File

@@ -7,7 +7,8 @@
//! using the [`Leptos`](https://github.com/leptos-rs/leptos) web framework.
//!
//! Document metadata is updated automatically when running in the browser. For server-side
//! rendering, after the component tree is rendered to HTML, [`ServerMetaContextOutput::inject_meta_context`] will inject meta tags into a stream of HTML inside the `<head>`.
//! rendering, after the component tree is rendered to HTML, [`MetaContext::dehydrate`] can generate
//! HTML that should be injected into the `<head>` of the HTML document being rendered.
//!
//! ```
//! use leptos::prelude::*;
@@ -37,17 +38,20 @@
//! }
//! ```
//! # Feature Flags
//! - `csr` Client-side rendering: Generate DOM nodes in the browser
//! - `ssr` Server-side rendering: Generate an HTML string (typically on the server)
//! - `tracing` Adds integration with the `tracing` crate.
//! - `hydrate` Hydration: use this to add interactivity to an SSRed Leptos app
//! - `stable` By default, Leptos requires `nightly` Rust, which is what allows the ergonomics
//! of calling signals as functions. Enable this feature to support `stable` Rust.
//!
//! **Important Note:** If youre using server-side rendering, you should enable `ssr`.
//! **Important Note:** You must enable one of `csr`, `hydrate`, or `ssr` to tell Leptos
//! which mode your app is operating in.
use futures::{Stream, StreamExt};
use leptos::{
attr::{any_attribute::AnyAttribute, NextAttribute},
attr::NextAttribute,
component,
logging::debug_warn,
oco::Oco,
reactive::owner::{provide_context, use_context},
tachys::{
dom::document,
@@ -57,8 +61,8 @@ use leptos::{
},
hydration::Cursor,
view::{
add_attr::AddAnyAttr, any_view::ExtraAttrsMut, Mountable, Position,
PositionState, Render, RenderHtml,
add_attr::AddAnyAttr, Mountable, Position, PositionState, Render,
RenderHtml,
},
},
IntoView,
@@ -334,7 +338,6 @@ where
&mut Position::NextChild,
false,
false,
None,
);
_ = cx.elements.send(buf); // fails only if the receiver is already dropped
} else {
@@ -391,17 +394,13 @@ where
{
type State = RegisteredMetaTagState<E, At, Ch>;
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
let state = self.el.unwrap().build(extra_attrs);
fn build(self) -> Self::State {
let state = self.el.unwrap().build();
RegisteredMetaTagState { state }
}
fn rebuild(
self,
state: &mut Self::State,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
self.el.unwrap().rebuild(&mut state.state, extra_attrs);
fn rebuild(self, state: &mut Self::State) {
self.el.unwrap().rebuild(&mut state.state);
}
}
@@ -434,18 +433,14 @@ where
Ch: RenderHtml + Send,
{
type AsyncOutput = Self;
type Owned = RegisteredMetaTag<E, At::CloneableOwned, Ch::Owned>;
const MIN_LENGTH: usize = 0;
fn dry_resolve(&mut self, extra_attrs: ExtraAttrsMut<'_>) {
self.el.dry_resolve(extra_attrs)
fn dry_resolve(&mut self) {
self.el.dry_resolve()
}
async fn resolve(
self,
_extra_attrs: ExtraAttrsMut<'_>,
) -> Self::AsyncOutput {
async fn resolve(self) -> Self::AsyncOutput {
self // TODO?
}
@@ -455,7 +450,6 @@ where
_position: &mut Position,
_escape: bool,
_mark_branches: bool,
_extra_attrs: Option<Vec<AnyAttribute>>,
) {
// meta tags are rendered into the buffer stored into the context
// the value has already been taken out, when we're on the server
@@ -465,7 +459,6 @@ where
self,
_cursor: &Cursor,
_position: &PositionState,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
let cursor = use_context::<MetaContext>()
.expect(
@@ -476,16 +469,9 @@ where
let state = self.el.unwrap().hydrate::<FROM_SERVER>(
&cursor,
&PositionState::new(Position::NextChild),
extra_attrs,
);
RegisteredMetaTagState { state }
}
fn into_owned(self) -> Self::Owned {
RegisteredMetaTag {
el: self.el.map(|inner| inner.into_owned()),
}
}
}
impl<E, At, Ch> Mountable for RegisteredMetaTagState<E, At, Ch>
@@ -538,14 +524,9 @@ struct MetaTagsView;
impl Render for MetaTagsView {
type State = ();
fn build(self, _extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {}
fn build(self) -> Self::State {}
fn rebuild(
self,
_state: &mut Self::State,
_extra_attrs: Option<Vec<AnyAttribute>>,
) {
}
fn rebuild(self, _state: &mut Self::State) {}
}
impl AddAnyAttr for MetaTagsView {
@@ -564,16 +545,12 @@ impl AddAnyAttr for MetaTagsView {
impl RenderHtml for MetaTagsView {
type AsyncOutput = Self;
type Owned = Self;
const MIN_LENGTH: usize = 0;
fn dry_resolve(&mut self, _extra_attrs: ExtraAttrsMut<'_>) {}
fn dry_resolve(&mut self) {}
async fn resolve(
self,
_extra_attrs: ExtraAttrsMut<'_>,
) -> Self::AsyncOutput {
async fn resolve(self) -> Self::AsyncOutput {
self
}
@@ -583,7 +560,6 @@ impl RenderHtml for MetaTagsView {
_position: &mut Position,
_escape: bool,
_mark_branches: bool,
_extra_attrs: Option<Vec<AnyAttribute>>,
) {
buf.push_str("<!--HEAD-->");
}
@@ -592,33 +568,6 @@ impl RenderHtml for MetaTagsView {
self,
_cursor: &Cursor,
_position: &PositionState,
_extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
}
fn into_owned(self) -> Self::Owned {
self
}
}
pub(crate) trait OrDefaultNonce {
fn or_default_nonce(self) -> Option<Oco<'static, str>>;
}
impl OrDefaultNonce for Option<Oco<'static, str>> {
fn or_default_nonce(self) -> Option<Oco<'static, str>> {
#[cfg(feature = "nonce")]
{
use leptos::nonce::use_nonce;
match self {
Some(nonce) => Some(nonce),
None => use_nonce().map(|n| Arc::clone(n.as_inner()).into()),
}
}
#[cfg(not(feature = "nonce"))]
{
self
}
}
}

Some files were not shown because too many files have changed in this diff Show More