Compare commits

..

1 Commits

Author SHA1 Message Date
Greg Johnston
c0df839e97 fix: wait for blocking resources before sending subsequent chunks (close #3280) 2024-11-23 09:19:14 -05:00
299 changed files with 5984 additions and 19001 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

@@ -13,7 +13,6 @@ concurrency:
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
DEBIAN_FRONTEND: noninteractive
jobs:
autofix:
runs-on: ubuntu-latest
@@ -22,10 +21,6 @@ jobs:
- 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: |

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
@@ -28,6 +26,5 @@ jobs:
uses: ./.github/workflows/run-cargo-make-task.yml
with:
directory: ${{ matrix.directory }}
erased_mode: ${{ matrix.erased_mode }}
cargo_make_task: "ci"
toolchain: stable

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
@@ -25,6 +23,5 @@ jobs:
uses: ./.github/workflows/run-cargo-make-task.yml
with:
directory: ${{ matrix.directory }}
erased_mode: ${{ matrix.erased_mode }}
cargo_make_task: "ci"
toolchain: stable

View File

@@ -4,30 +4,22 @@ 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'
name: Run semver check (nightly-2025-03-05)
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
uses: obi1kenobi/cargo-semver-checks-action@v2
with:
rust-toolchain: nightly-2025-03-05
rust-toolchain: nightly-2024-08-01

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
@@ -25,6 +23,5 @@ jobs:
uses: ./.github/workflows/run-cargo-make-task.yml
with:
directory: ${{ matrix.directory }}
erased_mode: ${{ matrix.erased_mode }}
cargo_make_task: "ci"
toolchain: nightly-2025-03-05
toolchain: nightly-2024-08-01

View File

@@ -50,5 +50,5 @@ jobs:
echo "matrix={\"directory\":${{ steps.changed-dirs.outputs.all_changed_files }}}" >> "$GITHUB_OUTPUT"
else
# Create matrix with one item to prevent an empty vector error
echo "matrix={\"directory\":[\"NO_CHANGE\"], \"erased_mode\": [false, true]}" >> "$GITHUB_OUTPUT"
echo "matrix={\"directory\":[\"NO_CHANGE\"]}" >> "$GITHUB_OUTPUT"
fi

View File

@@ -28,7 +28,7 @@ jobs:
sed 's/\/$//' |
jq -R -s -c 'split("\n")[:-1]')
echo "Example Directories: $examples"
echo "matrix={\"directory\":$examples, \"erased_mode\": [false, true]}" >> "$GITHUB_OUTPUT"
echo "matrix={\"directory\":$examples}" >> "$GITHUB_OUTPUT"
- name: Print Location Info
run: |
echo "Workspace: ${{ github.workspace }}"

View File

@@ -24,7 +24,7 @@ jobs:
sed "s|$(pwd)/||" |
jq -R -s -c 'split("\n")[:-1]')
echo "Leptos Directories: $crates"
echo "matrix={\"directory\":$crates, \"erased_mode\": [false, true]}" >> "$GITHUB_OUTPUT"
echo "matrix={\"directory\":$crates}" >> "$GITHUB_OUTPUT"
- name: Print Location Info
run: |
echo "Workspace: ${{ github.workspace }}"

View File

@@ -5,9 +5,6 @@ on:
directory:
required: true
type: string
erased_mode:
required: true
type: boolean
cargo_make_task:
required: true
type: string
@@ -17,11 +14,9 @@ on:
env:
CARGO_TERM_COLOR: always
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
DEBIAN_FRONTEND: noninteractive
RUSTFLAGS: ${{ inputs.erased_mode && '--cfg erase_components' || '' }}
jobs:
test:
name: "Run ${{ inputs.cargo_make_task }} (${{ inputs.toolchain }}) (erased_mode: ${{ inputs.erased_mode }})"
name: Run ${{ inputs.cargo_make_task }} (${{ inputs.toolchain }})
runs-on: ubuntu-latest
steps:
- name: Free Disk Space
@@ -38,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
@@ -59,7 +50,7 @@ jobs:
- name: Install wasm-bindgen
run: cargo binstall wasm-bindgen-cli --no-confirm
- name: Install cargo-leptos
run: cargo binstall cargo-leptos --locked --no-confirm
run: cargo binstall cargo-leptos --no-confirm
- name: Install Trunk
uses: jetli/trunk-action@v0.5.0
with:

1475
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -40,39 +40,36 @@ members = [
exclude = ["benchmarks", "examples", "projects"]
[workspace.package]
version = "0.8.0-alpha"
version = "0.7.0-rc2"
edition = "2021"
rust-version = "1.76"
[workspace.dependencies]
throw_error = { path = "./any_error/", version = "0.3.0" }
any_spawner = { path = "./any_spawner/", version = "0.2.0" }
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1" }
either_of = { path = "./either_of/", version = "0.1.5" }
hydration_context = { path = "./hydration_context", version = "0.3.0" }
itertools = "0.14.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" }
throw_error = { path = "./any_error/", version = "0.2.0-rc2" }
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-rc2" }
leptos = { path = "./leptos", version = "0.7.0-rc2" }
leptos_config = { path = "./leptos_config", version = "0.7.0-rc2" }
leptos_dom = { path = "./leptos_dom", version = "0.7.0-rc2" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.0-rc2" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.0-rc2" }
leptos_macro = { path = "./leptos_macro", version = "0.7.0-rc2" }
leptos_router = { path = "./router", version = "0.7.0-rc2" }
leptos_router_macro = { path = "./router_macro", version = "0.7.0-rc2" }
leptos_server = { path = "./leptos_server", version = "0.7.0-rc2" }
leptos_meta = { path = "./meta", version = "0.7.0-rc2" }
next_tuple = { path = "./next_tuple", version = "0.1.0-rc2" }
oco_ref = { path = "./oco", version = "0.2.0" }
or_poisoned = { path = "./or_poisoned", version = "0.1.0" }
reactive_graph = { path = "./reactive_graph", version = "0.2.0-alpha" }
reactive_stores = { path = "./reactive_stores", version = "0.2.0-alpha" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.2.0-alpha" }
serde_json = "1.0.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.2.0-alpha" }
wasm-bindgen = { version = "0.2.100" }
reactive_graph = { path = "./reactive_graph", version = "0.1.0-rc2" }
reactive_stores = { path = "./reactive_stores", version = "0.1.0-rc2" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0-rc2" }
server_fn = { path = "./server_fn", version = "0.7.0-rc2" }
server_fn_macro = { path = "./server_fn_macro", version = "0.7.0-rc2" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.0-rc2" }
tachys = { path = "./tachys", version = "0.1.0-rc2" }
[profile.release]
codegen-units = 1
@@ -81,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
@@ -21,7 +22,7 @@ use leptos::*;
#[component]
pub fn SimpleCounter(initial_value: i32) -> impl IntoView {
// create a reactive signal with the initial value
let (value, set_value) = signal(initial_value);
let (value, set_value) = create_signal(initial_value);
// create event handlers for our buttons
// note that `value` and `set_value` are `Copy`, so it's super easy to move them into closures
@@ -46,7 +47,7 @@ pub fn SimpleCounter(initial_value: i32) -> impl IntoView {
pub fn SimpleCounterWithBuilder(initial_value: i32) -> impl IntoView {
use leptos::html::*;
let (value, set_value) = signal(initial_value);
let (value, set_value) = create_signal(initial_value);
let clear = move |_| set_value(0);
let decrement = move |_| set_value.update(|value| *value -= 1);
let increment = move |_| set_value.update(|value| *value += 1);
@@ -168,14 +169,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-rc2"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -9,7 +9,7 @@ use std::{
error,
fmt::{self, Display},
future::Future,
ops,
mem, ops,
pin::Pin,
sync::Arc,
task::{Context, Poll},
@@ -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)]
@@ -104,7 +109,7 @@ pub fn get_error_hook() -> Option<Arc<dyn ErrorHook>> {
/// Sets the current thread-local error hook, which will be invoked when [`throw`] is called.
pub fn set_error_hook(hook: Arc<dyn ErrorHook>) -> ResetErrorHookOnDrop {
ResetErrorHookOnDrop(
ERROR_HOOK.with_borrow_mut(|this| Option::replace(this, hook)),
ERROR_HOOK.with_borrow_mut(|this| mem::replace(this, Some(hook))),
)
}

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"
@@ -16,8 +16,8 @@ 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.50", 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

@@ -23,7 +23,7 @@ tokio-test = "0.4.0"
miniserde = "0.1.0"
gloo = "0.8.0"
uuid = { version = "1.0", features = ["serde", "v4", "wasm-bindgen"] }
wasm-bindgen = "0.2.100"
wasm-bindgen = "0.2.0"
lazy_static = "1.0"
log = "0.4.0"
strum = "0.24.0"

View File

@@ -1,6 +1,6 @@
[package]
name = "either_of"
version = "0.1.5"
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

@@ -1,6 +1,5 @@
[tasks.install-cargo-leptos]
install_crate = { crate_name = "cargo-leptos", binary = "cargo-leptos", test_arg = "--help" }
args = ["--locked"]
[tasks.cargo-leptos-e2e]
command = "cargo"

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

@@ -1,4 +1,5 @@
use leptos::prelude::*;
use leptos::tachys::html::style::style;
use serde::{Deserialize, Serialize};
use thiserror::Error;
@@ -15,7 +16,7 @@ pub enum CatError {
type CatCount = usize;
async fn fetch_cats(count: CatCount) -> Result<Vec<String>, Error> {
async fn fetch_cats(count: CatCount) -> Result<Vec<String>> {
if count > 0 {
gloo_timers::future::TimeoutFuture::new(1000).await;
// make the request
@@ -41,7 +42,11 @@ async fn fetch_cats(count: CatCount) -> Result<Vec<String>, Error> {
pub fn fetch_example() -> impl IntoView {
let (cat_count, set_cat_count) = signal::<CatCount>(1);
let cats = LocalResource::new(move || fetch_cats(cat_count.get()));
// we use new_unsync here because the reqwasm request type isn't Send
// if we were doing SSR, then
// 1) we'd want to use a Resource, so the data would be serialized to the client
// 2) we'd need to make sure there was a thread-local spawner set up
let cats = AsyncDerived::new_unsync(move || fetch_cats(cat_count.get()));
let fallback = move |errors: ArcRwSignal<Errors>| {
let error_list = move || {
@@ -61,6 +66,8 @@ pub fn fetch_example() -> impl IntoView {
}
};
let spreadable = style(("background-color", "AliceBlue"));
view! {
<div>
<label>
@@ -75,7 +82,7 @@ pub fn fetch_example() -> impl IntoView {
/>
</label>
<Transition fallback=|| view! { <div>"Loading..."</div> }>
<Transition fallback=|| view! { <div>"Loading..."</div> } {..spreadable}>
<ErrorBoundary fallback>
<ul>
{move || Suspend::new(async move {
@@ -85,7 +92,7 @@ pub fn fetch_example() -> impl IntoView {
.map(|s| {
view! {
<li>
<img src=s.clone() />
<img src=s.clone()/>
</li>
}
})

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

@@ -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",
"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,20 +10,22 @@ 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",
"islands",
] }
leptos_router = { path = "../../router" }
server_fn = { path = "../../server_fn", features = ["serde-lite"] }
leptos_axum = { path = "../../integrations/axum", features = [
"islands-router",
"dont-use-islands-router",
], 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 }
wasm-bindgen = "0.2.100"
serde_json = "1.0.133"
wasm-bindgen = "0.2.93"
[features]
hydrate = ["leptos/hydrate"]
@@ -56,11 +58,11 @@ site-root = "target/site"
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "style.css"
style-file = "./style.css"
# [Optional] Files in the asset-dir will be copied to the site-root directory
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "127.0.0.1:3009"
site-addr = "127.0.0.1:3000"
# The port to use for automatic reload monitoring
reload-port = 3001
# The browserlist query used for optimizing the CSS.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,140 @@
window.addEventListener("click", async (ev) => {
// confirm that this is an <a> that meets our requirements
if (
ev.defaultPrevented ||
ev.button !== 0 ||
ev.metaKey ||
ev.altKey ||
ev.ctrlKey ||
ev.shiftKey
)
return;
/** @type HTMLAnchorElement | undefined;*/
const a = ev
.composedPath()
.find(el => el instanceof Node && el.nodeName.toUpperCase() === "A");
if (!a) return;
const svg = a.namespaceURI === "http://www.w3.org/2000/svg";
const href = svg ? a.href.baseVal : a.href;
const target = svg ? a.target.baseVal : a.target;
if (target || (!href && !a.hasAttribute("state"))) return;
const rel = (a.getAttribute("rel") || "").split(/\s+/);
if (a.hasAttribute("download") || (rel && rel.includes("external"))) return;
const url = svg ? new URL(href, document.baseURI) : new URL(href);
if (
url.origin !== window.location.origin // ||
// TODO base
//(basePath && url.pathname && !url.pathname.toLowerCase().startsWith(basePath.toLowerCase()))
)
return;
ev.preventDefault();
// fetch the new page
const resp = await fetch(url);
const htmlString = await resp.text();
// Use DOMParser to parse the HTML string
const parser = new DOMParser();
// 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 () => {
const oldDocWalker = document.createTreeWalker(document);
const newDocWalker = doc.createTreeWalker(doc);
let oldNode = oldDocWalker.currentNode;
let newNode = newDocWalker.currentNode;
while(oldDocWalker.nextNode() && newDocWalker.nextNode()) {
oldNode = oldDocWalker.currentNode;
newNode = newDocWalker.currentNode;
// if the nodes are different, we need to replace the old with the new
// because of the typed view tree, this should never actually happen
if (oldNode.nodeType !== newNode.nodeType) {
oldNode.replaceWith(newNode);
}
// if it's a text node, just update the text with the new text
else if (oldNode.nodeType === Node.TEXT_NODE) {
oldNode.textContent = newNode.textContent;
}
// if it's an element, replace if it's a different tag, or update attributes
else if (oldNode.nodeType === Node.ELEMENT_NODE) {
/** @type Element */
const oldEl = oldNode;
/** @type Element */
const newEl = newNode;
if (oldEl.tagName !== newEl.tagName) {
oldEl.replaceWith(newEl);
}
else {
for(const attr of newEl.attributes) {
oldEl.setAttribute(attr.name, attr.value);
}
}
}
// we use comment "branch marker" nodes to distinguish between different branches in the statically-typed view tree
// if one of these marker is hit, then there are two options
// 1) it's the same branch, and we just keep walking until the end
// 2) it's a different branch, in which case the old can be replaced with the new wholesale
else if (oldNode.nodeType === Node.COMMENT_NODE) {
const oldText = oldNode.textContent;
const newText = newNode.textContent;
if(oldText.startsWith("bo") && newText !== oldText) {
oldDocWalker.nextNode();
newDocWalker.nextNode();
const oldRange = new Range();
const newRange = new Range();
let oldBranches = 1;
let newBranches = 1;
while(oldBranches > 0 && newBranches > 0) {
if(oldDocWalker.nextNode() && newDocWalker.nextNode()) {
console.log(oldDocWalker.currentNode, newDocWalker.currentNode);
if(oldDocWalker.currentNode.nodeType === Node.COMMENT_NODE) {
if(oldDocWalker.currentNode.textContent.startsWith("bo")) {
oldBranches += 1;
} else if(oldDocWalker.currentNode.textContent.startsWith("bc")) {
oldBranches -= 1;
}
}
if(newDocWalker.currentNode.nodeType === Node.COMMENT_NODE) {
if(newDocWalker.currentNode.textContent.startsWith("bo")) {
newBranches += 1;
} else if(newDocWalker.currentNode.textContent.startsWith("bc")) {
newBranches -= 1;
}
}
}
}
try {
oldRange.setStartAfter(oldNode);
oldRange.setEndBefore(oldDocWalker.currentNode);
newRange.setStartAfter(newNode);
newRange.setEndBefore(newDocWalker.currentNode);
const newContents = newRange.extractContents();
oldRange.deleteContents();
oldRange.insertNode(newContents);
oldNode.replaceWith(newNode);
oldDocWalker.currentNode.replaceWith(newDocWalker.currentNode);
} catch (e) {
console.error(e);
}
} }
}
};
// Not all browsers support startViewTransition; see https://caniuse.com/?search=startViewTransition
if (document.startViewTransition) {
await document.startViewTransition(transition);
} else {
await transition()
}
window.history.pushState(undefined, null, url);
});

View File

@@ -1,13 +1,8 @@
use leptos::{
either::{Either, EitherOf3},
prelude::*,
};
use leptos::prelude::*;
use leptos_router::{
components::{Route, Router, Routes},
hooks::{use_params_map, use_query_map},
path,
components::{FlatRoutes, Route, Router},
StaticSegment,
};
use serde::{Deserialize, Serialize};
pub fn shell(options: LeptosOptions) -> impl IntoView {
view! {
@@ -17,7 +12,7 @@ pub fn shell(options: LeptosOptions) -> impl IntoView {
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<AutoReload options=options.clone()/>
<HydrationScripts options=options islands=true islands_router=true/>
<HydrationScripts options=options islands=true/>
<link rel="stylesheet" id="leptos" href="/pkg/islands.css"/>
<link rel="shortcut icon" type="image/ico" href="/favicon.ico"/>
</head>
@@ -31,180 +26,34 @@ pub fn shell(options: LeptosOptions) -> impl IntoView {
#[component]
pub fn App() -> impl IntoView {
view! {
<script src="/routing.js"></script>
<Router>
<header>
<h1>"My Contacts"</h1>
<h1>"My Application"</h1>
</header>
<nav>
<a href="/">"Home"</a>
<a href="/about">"About"</a>
<a href="/">"Page A"</a>
<a href="/b">"Page B"</a>
</nav>
<main>
<Routes fallback=|| "Not found.">
<Route path=path!("") view=Home/>
<Route path=path!("user/:id") view=Details/>
<Route path=path!("about") view=About/>
</Routes>
<p>
<label>"Home Checkbox" <input type="checkbox"/></label>
</p>
<FlatRoutes fallback=|| "Not found.">
<Route path=StaticSegment("") view=PageA/>
<Route path=StaticSegment("b") view=PageB/>
</FlatRoutes>
</main>
</Router>
}
}
#[server]
pub async fn search(query: String) -> Result<Vec<User>, ServerFnError> {
let users = tokio::fs::read_to_string("./mock_data.json").await?;
let data: Vec<User> = serde_json::from_str(&users)?;
let query = query.to_ascii_lowercase();
Ok(data
.into_iter()
.filter(|user| {
user.first_name.to_ascii_lowercase().contains(&query)
|| user.last_name.to_ascii_lowercase().contains(&query)
|| user.email.to_ascii_lowercase().contains(&query)
})
.collect())
}
#[server]
pub async fn delete_user(id: u32) -> Result<(), ServerFnError> {
let users = tokio::fs::read_to_string("./mock_data.json").await?;
let mut data: Vec<User> = serde_json::from_str(&users)?;
data.retain(|user| user.id != id);
let new_json = serde_json::to_string(&data)?;
tokio::fs::write("./mock_data.json", &new_json).await?;
Ok(())
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct User {
id: u32,
first_name: String,
last_name: String,
email: String,
#[component]
pub fn PageA() -> impl IntoView {
view! { <label>"Page A" <input type="checkbox"/></label> }
}
#[component]
pub fn Home() -> impl IntoView {
let q = use_query_map();
let q = move || q.read().get("q");
let data = Resource::new(q, |q| async move {
if let Some(q) = q {
search(q).await
} else {
Ok(vec![])
}
});
let delete_user_action = ServerAction::<DeleteUser>::new();
let view = move || {
Suspend::new(async move {
let users = data.await.unwrap();
if q().is_none() {
EitherOf3::A(view! {
<p class="note">"Enter a search to begin viewing contacts."</p>
})
} else if users.is_empty() {
EitherOf3::B(view! {
<p class="note">"No users found matching that search."</p>
})
} else {
EitherOf3::C(view! {
<table>
<tbody>
<For
each=move || users.clone()
key=|user| user.id
let:user
>
<tr>
<td>{user.first_name}</td>
<td>{user.last_name}</td>
<td>{user.email}</td>
<td>
<a href=format!("/user/{}", user.id)>"Details"</a>
<input type="checkbox"/>
<ActionForm action=delete_user_action>
<input type="hidden" name="id" value=user.id/>
<input type="submit" value="Delete"/>
</ActionForm>
</td>
</tr>
</For>
</tbody>
</table>
})
}
})
};
view! {
<section class="page">
<form method="GET" class="search">
<input type="search" name="q" value=q autofocus oninput="this.form.requestSubmit()"/>
<input type="submit"/>
</form>
<Suspense fallback=|| view! { <p>"Loading..."</p> }>{view}</Suspense>
</section>
}
}
#[component]
pub fn Details() -> impl IntoView {
#[server]
pub async fn get_user(id: u32) -> Result<Option<User>, ServerFnError> {
let users = tokio::fs::read_to_string("./mock_data.json").await?;
let data: Vec<User> = serde_json::from_str(&users)?;
Ok(data.iter().find(|user| user.id == id).cloned())
}
let params = use_params_map();
let id = move || {
params
.read()
.get("id")
.and_then(|id| id.parse::<u32>().ok())
};
let user = Resource::new(id, |id| async move {
match id {
None => Ok(None),
Some(id) => get_user(id).await,
}
});
move || {
Suspend::new(async move {
user.await.map(|user| match user {
None => Either::Left(view! {
<section class="page">
<h2>"Not found."</h2>
<p>"Sorry — we couldnt find that user."</p>
</section>
}),
Some(user) => Either::Right(view! {
<section class="page">
<h2>{user.first_name} " " { user.last_name}</h2>
<p class="email">{user.email}</p>
</section>
}),
})
})
}
}
#[component]
pub fn About() -> impl IntoView {
view! {
<section class="page">
<h2>"About"</h2>
<p>"This demo is intended to show off an experimental “islands router” feature, which mimics the smooth transitions and user experience of client-side routing while minimizing the amount of code that actually runs in the browser."</p>
<p>"By default, all the content in this application is only rendered on the server. But you can add client-side interactivity via islands like this one:"</p>
<Counter/>
</section>
}
}
#[island]
pub fn Counter() -> impl IntoView {
let count = RwSignal::new(0);
view! {
<button class="counter" on:click=move |_| *count.write() += 1>{count}</button>
}
pub fn PageB() -> impl IntoView {
view! { <label>"Page B" <input type="checkbox"/></label> }
}

View File

@@ -1,52 +1,3 @@
body {
font-family: system-ui, sans-serif;
background-color: #f6f6fa;
}
h1, h2, h3, h4, h5, h6 {
font-family: ui-rounded, 'Hiragino Maru Gothic ProN', Quicksand, Comfortaa, Manjari, 'Arial Rounded MT', 'Arial Rounded MT Bold', Calibri, source-sans-pro, sans-serif;
text-align: center;
}
nav {
padding: 1rem;
text-align: center;
}
nav a {
margin: 1rem;
}
form.search {
display: flex;
margin: 2rem auto;
justify-content: center;
}
td {
min-width: 10rem;
width: 10rem;
}
table {
min-width: 100%;
}
.page {
width: 80%;
margin: auto;
}
td:last-child > * {
display: inline-block;
}
.note, .note {
text-align: center;
}
button.counter {
display: block;
font-size: 2rem;
margin: auto;
.pending {
color: purple;
}

View File

@@ -149,12 +149,12 @@ pub fn App() -> impl IntoView {
</div>
<div class="col-md-6">
<div class="row">
<Button id="run" text="Create 1,000 rows" on:click=run />
<Button id="runlots" text="Create 10,000 rows" on:click=run_lots />
<Button id="add" text="Append 1,000 rows" on:click=add />
<Button id="update" text="Update every 10th row" on:click=update />
<Button id="clear" text="Clear" on:click=clear />
<Button id="swaprows" text="Swap Rows" on:click=swap_rows />
<Button id="run" text="Create 1,000 rows" on:click=run/>
<Button id="runlots" text="Create 10,000 rows" on:click=run_lots/>
<Button id="add" text="Append 1,000 rows" on:click=add/>
<Button id="update" text="Update every 10th row" on:click=update/>
<Button id="clear" text="Clear" on:click=clear/>
<Button id="swaprows" text="Swap Rows" on:click=swap_rows/>
</div>
</div>
</div>

View File

@@ -9,6 +9,7 @@ use leptos_router::{
},
hooks::{use_navigate, use_params, use_query_map},
params::Params,
MatchNestedRoutes,
};
use leptos_router_macro::path;
use std::time::Duration;
@@ -32,7 +33,7 @@ pub fn RouterExample() -> impl IntoView {
<Router set_is_routing>
// shows a progress bar while async data are loading
<div class="routing-progress">
<RoutingProgress is_routing max_time=Duration::from_millis(250) />
<RoutingProgress is_routing max_time=Duration::from_millis(250)/>
</div>
<nav>
// ordinary <a> elements can be used for client-side navigation
@@ -52,15 +53,15 @@ pub fn RouterExample() -> impl IntoView {
<Routes transition=true fallback=|| "This page could not be found.">
// paths can be created using the path!() macro, or provided as types like
// StaticSegment("about")
<Route path=path!("about") view=About />
<Route path=path!("about") view=About/>
<ProtectedRoute
path=path!("settings")
condition=move || Some(logged_in.get())
redirect_path=|| "/"
view=Settings
/>
<Route path=path!("redirect-home") view=|| view! { <Redirect path="/" /> } />
<ContactRoutes />
<Route path=path!("redirect-home") view=|| view! { <Redirect path="/"/> }/>
<ContactRoutes/>
</Routes>
</main>
</Router>
@@ -70,11 +71,11 @@ pub fn RouterExample() -> impl IntoView {
// You can define other routes in their own component.
// Routes implement the MatchNestedRoutes
#[component(transparent)]
pub fn ContactRoutes() -> impl leptos_router::MatchNestedRoutes + Clone {
pub fn ContactRoutes() -> impl MatchNestedRoutes + Clone {
view! {
<ParentRoute path=path!("") view=ContactList>
<Route path=path!("/") view=|| "Select a contact." />
<Route path=path!("/:id") view=Contact />
<Route path=path!("/") view=|| "Select a contact."/>
<Route path=path!("/:id") view=Contact/>
</ParentRoute>
}
.into_inner()
@@ -121,7 +122,7 @@ pub fn ContactList() -> impl IntoView {
<Suspense fallback=move || view! { <p>"Loading contacts..."</p> }>
<ul>{contacts}</ul>
</Suspense>
<Outlet />
<Outlet/>
</div>
}
}
@@ -165,7 +166,7 @@ pub fn Contact() -> impl IntoView {
Some(contact) => Either::Right(view! {
<section class="card">
<h1>{contact.first_name} " " {contact.last_name}</h1>
<p>{contact.address_1} <br /> {contact.address_2}</p>
<p>{contact.address_1} <br/> {contact.address_2}</p>
</section>
}),
}
@@ -223,10 +224,10 @@ pub fn Settings() -> impl IntoView {
<Form action="">
<fieldset>
<legend>"Name"</legend>
<input type="text" name="first_name" placeholder="First" />
<input type="text" name="last_name" placeholder="Last" />
<input type="text" name="first_name" placeholder="First"/>
<input type="text" name="last_name" placeholder="Last"/>
</fieldset>
<input type="submit" />
<input type="submit"/>
<p>
"This uses the " <code>"<Form/>"</code>
" component, which enhances forms by using client-side navigation for "

View File

@@ -21,21 +21,21 @@ 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 }
tower = { version = "0.5.2", optional = true }
tower-http = { version = "0.6.2", features = [
axum = { version = "0.7.5", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", features = [
"fs",
"tracing",
"trace",
], optional = true }
tokio = { version = "1.39", features = ["full"], optional = true }
thiserror = "2.0.11"
thiserror = "1.0"
wasm-bindgen = "0.2.93"
serde_toml = "0.0.1"
toml = "0.8.19"
web-sys = { version = "0.3.70", features = ["FileList", "File"] }
strum = { version = "0.27.1", features = ["strum_macros", "derive"] }
notify = { version = "8.0", optional = true }
strum = { version = "0.26.3", features = ["strum_macros", "derive"] }
notify = { version = "6.1", optional = true }
pin-project-lite = "0.2.14"
dashmap = { version = "6.0", optional = true }
once_cell = { version = "1.19", optional = true }

View File

@@ -1,4 +1,4 @@
use futures::{Sink, Stream, StreamExt};
use futures::StreamExt;
use http::Method;
use leptos::{html::Input, prelude::*, task::spawn_local};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
@@ -9,10 +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},
ContentType,
response::{browser::BrowserResponse, ClientRes, Res},
};
use std::future::Future;
#[cfg(feature = "ssr")]
@@ -654,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>
@@ -734,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>
}
}
@@ -762,11 +717,8 @@ pub struct Toml;
#[derive(Serialize, Deserialize)]
pub struct TomlEncoded<T>(T);
impl ContentType for Toml {
const CONTENT_TYPE: &'static str = "application/toml";
}
impl Encoding for Toml {
const CONTENT_TYPE: &'static str = "application/toml";
const METHOD: Method = Method::POST;
}
@@ -774,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)
}
}
@@ -788,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)
}
}
@@ -816,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()))
}
}
@@ -885,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;
@@ -897,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();
@@ -906,24 +854,6 @@ pub fn CustomClientExample() -> impl IntoView {
// delegate back out to BrowserClient to send the modified request
BrowserClient::send(req)
}
fn open_websocket(
path: &str,
) -> impl Future<
Output = Result<
(
impl Stream<Item = Result<server_fn::Bytes, E>> + Send + 'static,
impl Sink<Result<server_fn::Bytes, E>> + Send + 'static,
),
E,
>,
> + Send {
BrowserClient::open_websocket(path)
}
fn spawn(future: impl Future<Output = ()> + Send + 'static) {
<BrowserClient as Client<E>>::spawn(future)
}
}
// Specify our custom client with `client = `

View File

@@ -1,2 +1,2 @@
[toolchain]
channel = "nightly-2025-03-05"
channel = "nightly-2024-01-29"

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

@@ -4,7 +4,7 @@ use leptos_router::{
hooks::use_params,
nested_router::Outlet,
params::Params,
ParamSegment, SsrMode, StaticSegment, WildcardSegment,
MatchNestedRoutes, ParamSegment, SsrMode, StaticSegment, WildcardSegment,
};
#[cfg(feature = "ssr")]
@@ -21,7 +21,6 @@ pub(super) mod counter {
pub struct Counter(AtomicU32);
impl Counter {
#[allow(dead_code)]
pub const fn new() -> Self {
Self(AtomicU32::new(0))
}
@@ -204,20 +203,20 @@ pub struct SuspenseCounters {
}
#[component]
pub fn InstrumentedRoutes() -> impl leptos_router::MatchNestedRoutes + Clone {
pub fn InstrumentedRoutes() -> impl MatchNestedRoutes + Clone {
// TODO should make this mode configurable via feature flag?
let ssr = SsrMode::Async;
view! {
<ParentRoute path=StaticSegment("instrumented") view=InstrumentedRoot ssr>
<Route path=StaticSegment("/") view=InstrumentedTop />
<Route path=StaticSegment("/") view=InstrumentedTop/>
<ParentRoute path=StaticSegment("item") view=ItemRoot>
<Route path=StaticSegment("/") view=ItemListing />
<Route path=StaticSegment("/") view=ItemListing/>
<ParentRoute path=ParamSegment("id") view=ItemTop>
<Route path=StaticSegment("/") view=ItemOverview />
<Route path=WildcardSegment("path") view=ItemInspect />
<Route path=StaticSegment("/") view=ItemOverview/>
<Route path=WildcardSegment("path") view=ItemInspect/>
</ParentRoute>
</ParentRoute>
<Route path=StaticSegment("counters") view=ShowCounters />
<Route path=StaticSegment("counters") view=ShowCounters/>
</ParentRoute>
}
.into_inner()
@@ -280,41 +279,32 @@ fn InstrumentedRoot() -> impl IntoView {
<section id="instrumented">
<nav>
<a href="/">"Site Root"</a>
<A href="./" exact=true>
"Instrumented Root"
</A>
<A href="item/" strict_trailing_slash=true>
"Item Listing"
</A>
<A href="counters" strict_trailing_slash=true>
"Counters"
</A>
<A href="./" exact=true>"Instrumented Root"</A>
<A href="item/" strict_trailing_slash=true>"Item Listing"</A>
<A href="counters" strict_trailing_slash=true>"Counters"</A>
</nav>
<FieldNavPortlet />
<Outlet />
<Suspense>
{move || Suspend::new(async move {
<FieldNavPortlet/>
<Outlet/>
<Suspense>{
move || Suspend::new(async move {
let clear_suspense_counters = move |_| {
counters.update(|c| *c = SuspenseCounters::default());
};
csr_ticket
.get()
.map(|ticket| {
let ticket = ticket.0;
view! {
<ActionForm action=reset_counters>
<input type="hidden" name="ticket" value=format!("{ticket}") />
<input
id="reset-csr-counters"
type="submit"
value="Reset CSR Counters"
on:click=clear_suspense_counters
/>
</ActionForm>
}
})
})}
</Suspense>
csr_ticket.get().map(|ticket| {
let ticket = ticket.0;
view! {
<ActionForm action=reset_counters>
<input type="hidden" name="ticket" value=format!("{ticket}") />
<input
id="reset-csr-counters"
type="submit"
value="Reset CSR Counters"
on:click=clear_suspense_counters/>
</ActionForm>
}
})
})
}</Suspense>
<footer>
<nav>
<A href="item/3/">"Target 3##"</A>
@@ -333,17 +323,11 @@ fn InstrumentedRoot() -> impl IntoView {
fn InstrumentedTop() -> impl IntoView {
view! {
<h1>"Instrumented Tests"</h1>
<p>
"These tests validates the number of invocations of server functions and suspenses per access."
</p>
<p>"These tests validates the number of invocations of server functions and suspenses per access."</p>
<ul>
// not using `A` because currently some bugs with artix
<li>
<a href="item/">"Item Listing"</a>
</li>
<li>
<a href="item/4/path1/">"Target 41#"</a>
</li>
<li><a href="item/">"Item Listing"</a></li>
<li><a href="item/4/path1/">"Target 41#"</a></li>
</ul>
}
}
@@ -358,7 +342,7 @@ fn ItemRoot() -> impl IntoView {
view! {
<h2>"<ItemRoot/>"</h2>
<Outlet />
<Outlet/>
}
}
@@ -376,9 +360,7 @@ fn ItemListing() -> impl IntoView {
// adding an extra `/` in artix; manually construct `a` instead.
// <li><A href=format!("./{item}/")>"Item "{item}</A></li>
view! {
<li>
<a href=format!("/instrumented/item/{item}/")>"Item "{item}</a>
</li>
<li><a href=format!("/instrumented/item/{item}/")>"Item "{item}</a></li>
}
)
.collect_view()
@@ -391,7 +373,9 @@ fn ItemListing() -> impl IntoView {
view! {
<h3>"<ItemListing/>"</h3>
<ul>
<Suspense>{item_listing}</Suspense>
<Suspense>
{item_listing}
</Suspense>
</ul>
}
}
@@ -418,7 +402,7 @@ fn ItemTop() -> impl IntoView {
));
view! {
<h4>"<ItemTop/>"</h4>
<Outlet />
<Outlet/>
}
}
@@ -428,29 +412,24 @@ fn ItemOverview() -> impl IntoView {
let resource = expect_context::<Resource<Option<GetItemResult>>>();
let item_view = move || {
Suspend::new(async move {
let result = resource.await.map(|GetItemResult(item, names)| {
view! {
<p>{format!("Viewing {item:?}")}</p>
<ul>
{names
.into_iter()
.map(|name| {
let id = item.id;
// FIXME seems like relative link isn't working, it is currently
// adding an extra `/` in artix; manually construct `a` instead.
// <li><A href=format!("./{name}/")>{format!("Inspect {name}")}</A></li>
view! {
<li>
<a href=format!(
"/instrumented/item/{id}/{name}/",
)>"Inspect "{name.clone()}</a>
</li>
}
})
.collect_view()}
</ul>
}
});
let result = resource.await.map(|GetItemResult(item, names)| view! {
<p>{format!("Viewing {item:?}")}</p>
<ul>{
names.into_iter()
.map(|name| {
// FIXME seems like relative link isn't working, it is currently
// adding an extra `/` in artix; manually construct `a` instead.
// <li><A href=format!("./{name}/")>{format!("Inspect {name}")}</A></li>
let id = item.id;
view! {
<li><a href=format!("/instrumented/item/{id}/{name}/")>
"Inspect "{name.clone()}
</a></li>
}
})
.collect_view()
}</ul>
});
suspense_counters.update_untracked(|c| c.item_overview += 1);
result
})
@@ -458,7 +437,9 @@ fn ItemOverview() -> impl IntoView {
view! {
<h5>"<ItemOverview/>"</h5>
<Suspense>{item_view}</Suspense>
<Suspense>
{item_view}
</Suspense>
}
}
@@ -515,26 +496,23 @@ fn ItemInspect() -> impl IntoView {
));
view! {
<p>{format!("Inspecting {item:?}")}</p>
<ul>
{fields
.iter()
<ul>{
fields.iter()
.map(|field| {
// FIXME seems like relative link to root for a wildcard isn't
// working as expected, so manually construct `a` instead.
// let text = format!("Inspect {name}/{field}");
// view! {
// <li><A href=format!("{field}")>{text}</A></li>
// <li><A href=format!("{field}")>{text}</A></li>
// }
view! {
<li>
<a href=format!(
"/instrumented/item/{id}/{name}/{field}",
)>{format!("Inspect {name}/{field}")}</a>
</li>
<li><a href=format!("/instrumented/item/{id}/{name}/{field}")>{
format!("Inspect {name}/{field}")
}</a></li>
}
})
.collect_view()}
</ul>
.collect_view()
}</ul>
}
});
suspense_counters.update_untracked(|c| c.item_inspect += 1);
@@ -549,7 +527,9 @@ fn ItemInspect() -> impl IntoView {
view! {
<h5>"<ItemInspect/>"</h5>
<Suspense>{inspect_view}</Suspense>
<Suspense>
{inspect_view}
</Suspense>
}
}
@@ -610,8 +590,7 @@ fn ShowCounters() -> impl IntoView {
id="reset-counters"
type="submit"
value="Reset Counters"
on:click=clear_suspense_counters
/>
on:click=clear_suspense_counters/>
</ActionForm>
}
})
@@ -622,23 +601,20 @@ fn ShowCounters() -> impl IntoView {
<h2>"Counters"</h2>
<h3 id="suspend-calls">"Suspend Calls"</h3>
{move || {
suspense_counters
.with(|c| {
view! {
<dl>
<dt>"item_listing"</dt>
<dd id="item_listing">{c.item_listing}</dd>
<dt>"item_overview"</dt>
<dd id="item_overview">{c.item_overview}</dd>
<dt>"item_inspect"</dt>
<dd id="item_inspect">{c.item_inspect}</dd>
</dl>
}
})
}}
{move || suspense_counters.with(|c| view! {
<dl>
<dt>"item_listing"</dt>
<dd id="item_listing">{c.item_listing}</dd>
<dt>"item_overview"</dt>
<dd id="item_overview">{c.item_overview}</dd>
<dt>"item_inspect"</dt>
<dd id="item_inspect">{c.item_inspect}</dd>
</dl>
})}
<Suspense>{counter_view}</Suspense>
<Suspense>
{counter_view}
</Suspense>
}
}
@@ -666,17 +642,17 @@ pub fn FieldNavPortlet() -> impl IntoView {
view! {
<div id="FieldNavPortlet">
<span>"FieldNavPortlet:"</span>
<nav>
{ctx
.0
.map(|ctx| {
ctx.into_iter()
.map(|FieldNavItem { href, text }| {
view! { <A href=href>{text}</A> }
})
.collect_view()
})}
</nav>
<nav>{
ctx.0.map(|ctx| {
ctx.into_iter()
.map(|FieldNavItem { href, text }| {
view! {
<A href=href>{text}</A>
}
})
.collect_view()
})
}</nav>
</div>
}
})

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

@@ -1,22 +0,0 @@
{
"name": "leptos-tailwind",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "leptos-tailwind",
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
"tailwindcss": "^4.0.0"
}
},
"node_modules/tailwindcss": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.0.tgz",
"integrity": "sha512-ULRPI3A+e39T7pSaf1xoi58AqqJxVCLg8F/uM5A3FadUbnyDTgltVnXJvdkTjwCOGA6NazqHVcwPJC5h2vRYVQ==",
"dev": true
}
}
}

View File

@@ -1,69 +0,0 @@
/*! tailwindcss v4.0.0 | MIT License | https://tailwindcss.com */
@tailwind base;
@tailwind components;
.relative {
position: relative;
}
.m-auto {
margin: auto;
}
.flex {
display: flex;
}
.min-h-screen {
min-height: 100vh;
}
.transform {
transform: var(--tw-rotate-x) var(--tw-rotate-y) var(--tw-rotate-z) var(--tw-skew-x) var(--tw-skew-y);
}
.flex-col {
flex-direction: column;
}
.flex-row-reverse {
flex-direction: row-reverse;
}
.flex-wrap {
flex-wrap: wrap;
}
.border-b-4 {
border-bottom-style: var(--tw-border-style);
border-bottom-width: 4px;
}
.border-l-2 {
border-left-style: var(--tw-border-style);
border-left-width: 2px;
}
.bg-gradient-to-tl {
--tw-gradient-position: to top left in oklab,;
background-image: linear-gradient(var(--tw-gradient-stops));
}
@property --tw-rotate-x {
syntax: "*";
inherits: false;
initial-value: rotateX(0);
}
@property --tw-rotate-y {
syntax: "*";
inherits: false;
initial-value: rotateY(0);
}
@property --tw-rotate-z {
syntax: "*";
inherits: false;
initial-value: rotateZ(0);
}
@property --tw-skew-x {
syntax: "*";
inherits: false;
initial-value: skewX(0);
}
@property --tw-skew-y {
syntax: "*";
inherits: false;
initial-value: skewY(0);
}
@property --tw-border-style {
syntax: "*";
inherits: false;
initial-value: solid;
}

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

@@ -1,6 +1,6 @@
[package]
name = "hydration_context"
version = "0.3.0"
version = "0.2.0-rc2"
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.100", 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

@@ -18,14 +18,13 @@ 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"] }
tachys = { workspace = true }
serde_json = { workspace = true }
serde_json = "1.0"
parking_lot = "0.12.3"
tracing = { version = "0.1", optional = true }
tokio = { version = "1.43", features = ["rt", "fs"] }
tokio = { version = "1.41", features = ["rt", "fs"] }
send_wrapper = "0.6.0"
dashmap = "6"
once_cell = "1"
@@ -34,7 +33,7 @@ once_cell = "1"
rustdoc-args = ["--generate-link-to-definition"]
[features]
islands-router = ["tachys/islands"]
dont-use-islands-router = []
tracing = ["dep:tracing"]
[package.metadata.cargo-all-features]

View File

@@ -23,7 +23,6 @@ use hydration_context::SsrSharedContext;
use leptos::{
config::LeptosOptions,
context::{provide_context, use_context},
hydration::IslandsRouterNavigation,
prelude::expect_context,
reactive::{computed::ScopedFuture, owner::Owner},
IntoView,
@@ -275,13 +274,14 @@ pub fn redirect(path: &str) {
///
/// This can then be set up at an appropriate route in your application:
///
/// ```no_run
/// ```
/// use actix_web::*;
///
/// fn register_server_functions() {
/// // call ServerFn::register() for each of the server functions you've defined
/// }
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[actix_web::main]
/// async fn main() -> std::io::Result<()> {
/// // make sure you actually register your server functions
@@ -297,6 +297,7 @@ pub fn redirect(path: &str) {
/// .run()
/// .await
/// }
/// # }
/// ```
///
/// ## Provided Context Types
@@ -368,13 +369,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 =
@@ -388,20 +390,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,7 +420,7 @@ pub fn handle_server_fns_with_context(
/// but requires some client-side JavaScript.
///
/// This can then be set up at an appropriate route in your application:
/// ```no_run
/// ```
/// use actix_web::{App, HttpServer};
/// use leptos::prelude::*;
/// use leptos_router::Method;
@@ -442,6 +431,7 @@ pub fn handle_server_fns_with_context(
/// view! { <main>"Hello, world!"</main> }
/// }
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[actix_web::main]
/// async fn main() -> std::io::Result<()> {
/// let conf = get_configuration(Some("Cargo.toml")).unwrap();
@@ -461,6 +451,7 @@ pub fn handle_server_fns_with_context(
/// .run()
/// .await
/// }
/// # }
/// ```
///
/// ## Provided Context Types
@@ -488,7 +479,7 @@ where
/// sending down its HTML. The app will become interactive once it has fully loaded.
///
/// This can then be set up at an appropriate route in your application:
/// ```no_run
/// ```
/// use actix_web::{App, HttpServer};
/// use leptos::prelude::*;
/// use leptos_router::Method;
@@ -499,6 +490,7 @@ where
/// view! { <main>"Hello, world!"</main> }
/// }
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[actix_web::main]
/// async fn main() -> std::io::Result<()> {
/// let conf = get_configuration(Some("Cargo.toml")).unwrap();
@@ -521,6 +513,7 @@ where
/// .run()
/// .await
/// }
/// # }
/// ```
///
/// ## Provided Context Types
@@ -546,7 +539,7 @@ where
/// `async` resources have loaded.
///
/// This can then be set up at an appropriate route in your application:
/// ```no_run
/// ```
/// use actix_web::{App, HttpServer};
/// use leptos::prelude::*;
/// use leptos_router::Method;
@@ -557,6 +550,7 @@ where
/// view! { <main>"Hello, world!"</main> }
/// }
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[actix_web::main]
/// async fn main() -> std::io::Result<()> {
/// let conf = get_configuration(Some("Cargo.toml")).unwrap();
@@ -576,6 +570,7 @@ where
/// .run()
/// .await
/// }
/// # }
/// ```
///
/// ## Provided Context Types
@@ -655,27 +650,12 @@ where
IV: IntoView + 'static,
{
_ = replace_blocks; // TODO
handle_response(
method,
additional_context,
app_fn,
|app, chunks, supports_ooo| {
Box::pin(async move {
let app = if cfg!(feature = "islands-router") {
if supports_ooo {
app.to_html_stream_out_of_order_branching()
} else {
app.to_html_stream_in_order_branching()
}
} else if supports_ooo {
app.to_html_stream_out_of_order()
} else {
app.to_html_stream_in_order()
};
Box::pin(app.chain(chunks())) as PinnedStream<String>
})
},
)
handle_response(method, additional_context, app_fn, |app, chunks| {
Box::pin(async move {
Box::pin(app.to_html_stream_out_of_order().chain(chunks()))
as PinnedStream<String>
})
})
}
/// Returns an Actix [struct@Route](actix_web::Route) that listens for a `GET` request and tries
@@ -701,21 +681,12 @@ pub fn render_app_to_stream_in_order_with_context<IV>(
where
IV: IntoView + 'static,
{
handle_response(
method,
additional_context,
app_fn,
|app, chunks, _supports_ooo| {
Box::pin(async move {
let app = if cfg!(feature = "islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
};
Box::pin(app.chain(chunks())) as PinnedStream<String>
})
},
)
handle_response(method, additional_context, app_fn, |app, chunks| {
Box::pin(async move {
Box::pin(app.to_html_stream_in_order().chain(chunks()))
as PinnedStream<String>
})
})
}
/// Returns an Actix [struct@Route](actix_web::Route) that listens for a `GET` request and tries
@@ -747,13 +718,12 @@ where
fn async_stream_builder<IV>(
app: IV,
chunks: BoxedFnOnce<PinnedStream<String>>,
_supports_ooo: bool,
) -> PinnedFuture<PinnedStream<String>>
where
IV: IntoView + 'static,
{
Box::pin(async move {
let app = if cfg!(feature = "islands-router") {
let app = if cfg!(feature = "dont-use-islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
@@ -793,7 +763,6 @@ fn leptos_corrected_path(req: &HttpRequest) -> String {
}
}
#[allow(clippy::type_complexity)]
fn handle_response<IV>(
method: Method,
additional_context: impl Fn() + 'static + Clone + Send,
@@ -801,7 +770,6 @@ fn handle_response<IV>(
stream_builder: fn(
IV,
BoxedFnOnce<PinnedStream<String>>,
bool,
) -> PinnedFuture<PinnedStream<String>>,
) -> Route
where
@@ -812,9 +780,6 @@ where
let add_context = additional_context.clone();
async move {
let is_island_router_navigation = cfg!(feature = "islands-router")
&& req.headers().get("Islands-Router").is_some();
let res_options = ResponseOptions::default();
let (meta_context, meta_output) = ServerMetaContext::new();
@@ -825,10 +790,6 @@ where
move || {
provide_contexts(req, &meta_context, &res_options);
add_context();
if is_island_router_navigation {
provide_context(IslandsRouterNavigation);
}
}
};
@@ -838,7 +799,6 @@ where
additional_context,
res_options,
stream_builder,
!is_island_router_navigation,
)
.await;
@@ -1129,7 +1089,6 @@ impl StaticRouteGenerator {
app_fn.clone(),
additional_context,
async_stream_builder,
false,
);
let sc = owner.shared_context().unwrap();

View File

@@ -4,14 +4,14 @@ authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
description = "Axum integrations for the Leptos web framework."
version = "0.8.0-alpha2"
version = { workspace = true }
rust-version.workspace = true
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.9", default-features = false, features = [
"matched-path",
] }
dashmap = "6"
@@ -19,31 +19,24 @@ 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 }
tachys = { workspace = true }
once_cell = "1"
parking_lot = "0.12.3"
tokio = { version = "1.43", default-features = false }
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 }
tracing = { version = "0.1.40", optional = true }
[dev-dependencies]
axum = "0.8.1"
tokio = { version = "1.43", features = ["net", "rt-multi-thread"] }
axum = "0.7.9"
tokio = { version = "1.41", features = ["net", "rt-multi-thread"] }
[features]
wasm = []
default = [
"tokio/fs",
"tokio/sync",
"tower-http/fs",
"tower/util",
"server_fn/axum",
]
islands-router = ["tachys/islands"]
default = ["tokio/fs", "tokio/sync", "tower-http/fs", "tower/util"]
dont-use-islands-router = []
tracing = ["dep:tracing"]
[package.metadata.docs.rs]

View File

@@ -1,6 +1,5 @@
#![forbid(unsafe_code)]
#![deny(missing_docs)]
#![allow(clippy::type_complexity)]
//! Provides functions to easily integrate Leptos with Axum.
//!
@@ -17,6 +16,7 @@
//! - `default`: supports running in a typical native Tokio/Axum environment
//! - `wasm`: with `default-features = false`, supports running in a JS Fetch-based
//! environment
//! - `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
@@ -279,11 +279,12 @@ pub fn generate_request_and_parts(
///
/// This can then be set up at an appropriate route in your application:
///
/// ```no_run
/// ```
/// use axum::{handler::Handler, routing::post, Router};
/// use leptos::prelude::*;
/// use std::net::SocketAddr;
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[cfg(feature = "default")]
/// #[tokio::main]
/// async fn main() {
@@ -299,9 +300,7 @@ pub fn generate_request_and_parts(
/// .await
/// .unwrap();
/// }
///
/// # #[cfg(not(feature = "default"))]
/// # fn main() { }
/// # }
/// ```
/// Leptos provides a generic implementation of `handle_server_fns`. If access to more specific parts of the Request is desired,
/// you can specify your own server fn handler based on this one and give it it's own route in the server macro.
@@ -370,6 +369,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);
@@ -398,8 +399,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 =
@@ -411,7 +412,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)
})
@@ -442,7 +443,7 @@ pub type PinnedHtmlStream =
/// to route it using [leptos_router], serving an HTML stream of your application.
///
/// This can then be set up at an appropriate route in your application:
/// ```no_run
/// ```
/// use axum::{handler::Handler, Router};
/// use leptos::{config::get_configuration, prelude::*};
/// use std::{env, net::SocketAddr};
@@ -452,6 +453,7 @@ pub type PinnedHtmlStream =
/// view! { <main>"Hello, world!"</main> }
/// }
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[cfg(feature = "default")]
/// #[tokio::main]
/// async fn main() {
@@ -470,9 +472,7 @@ pub type PinnedHtmlStream =
/// .await
/// .unwrap();
/// }
///
/// # #[cfg(not(feature = "default"))]
/// # fn main() { }
/// # }
/// ```
///
/// ## Provided Context Types
@@ -485,7 +485,7 @@ pub type PinnedHtmlStream =
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>>
@@ -509,7 +509,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>,
@@ -531,7 +531,7 @@ where
/// sending down its HTML. The app will become interactive once it has fully loaded.
///
/// This can then be set up at an appropriate route in your application:
/// ```no_run
/// ```
/// use axum::{handler::Handler, Router};
/// use leptos::{config::get_configuration, prelude::*};
/// use std::{env, net::SocketAddr};
@@ -541,6 +541,7 @@ where
/// view! { <main>"Hello, world!"</main> }
/// }
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[cfg(feature = "default")]
/// #[tokio::main]
/// async fn main() {
@@ -559,9 +560,7 @@ where
/// .await
/// .unwrap();
/// }
///
/// # #[cfg(not(feature = "default"))]
/// # fn main() { }
/// # }
/// ```
///
/// ## Provided Context Types
@@ -574,7 +573,7 @@ where
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>>
@@ -627,14 +626,13 @@ where
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,
@@ -657,8 +655,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>,
@@ -759,32 +757,25 @@ where
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,
{
_ = replace_blocks; // TODO
handle_response(additional_context, app_fn, |app, chunks, supports_ooo| {
handle_response(additional_context, app_fn, |app, chunks| {
Box::pin(async move {
let app = if cfg!(feature = "islands-router") {
if supports_ooo {
app.to_html_stream_out_of_order_branching()
} else {
app.to_html_stream_in_order_branching()
}
} else if supports_ooo {
app.to_html_stream_out_of_order()
let app = if cfg!(feature = "dont-use-islands-router") {
app.to_html_stream_out_of_order_branching()
} else {
app.to_html_stream_in_order()
app.to_html_stream_out_of_order()
};
Box::pin(app.chain(chunks())) as PinnedStream<String>
})
@@ -833,8 +824,8 @@ where
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>>
@@ -844,8 +835,8 @@ pub fn render_app_to_stream_in_order_with_context<IV>(
where
IV: IntoView + 'static,
{
handle_response(additional_context, app_fn, |app, chunks, _supports_ooo| {
let app = if cfg!(feature = "islands-router") {
handle_response(additional_context, app_fn, |app, chunks| {
let app = if cfg!(feature = "dont-use-islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
@@ -857,18 +848,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>>,
bool,
) -> 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,
{
@@ -886,16 +872,12 @@ fn handle_response_inner<IV>(
stream_builder: fn(
IV,
BoxedFnOnce<PinnedStream<String>>,
bool,
) -> PinnedFuture<PinnedStream<String>>,
) -> PinnedFuture<Response<Body>>
where
IV: IntoView + 'static,
{
Box::pin(async move {
let is_island_router_navigation = cfg!(feature = "islands-router")
&& req.headers().get("Islands-Router").is_some();
let add_context = additional_context.clone();
let res_options = ResponseOptions::default();
let (meta_context, meta_output) = ServerMetaContext::new();
@@ -917,10 +899,6 @@ where
res_options.clone(),
);
add_context();
if is_island_router_navigation {
provide_context(IslandsRouterNavigation);
}
}
};
@@ -930,7 +908,6 @@ where
additional_context,
res_options,
stream_builder,
!is_island_router_navigation,
)
.await;
@@ -961,7 +938,7 @@ fn provide_contexts(
/// `async` resources have loaded.
///
/// This can then be set up at an appropriate route in your application:
/// ```no_run
/// ```
/// use axum::{handler::Handler, Router};
/// use leptos::{config::get_configuration, prelude::*};
/// use std::{env, net::SocketAddr};
@@ -971,6 +948,7 @@ fn provide_contexts(
/// view! { <main>"Hello, world!"</main> }
/// }
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[cfg(feature = "default")]
/// #[tokio::main]
/// async fn main() {
@@ -990,9 +968,7 @@ fn provide_contexts(
/// .await
/// .unwrap();
/// }
///
/// # #[cfg(not(feature = "default"))]
/// # fn main() { }
/// # }
/// ```
///
/// ## Provided Context Types
@@ -1005,7 +981,7 @@ fn provide_contexts(
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>>
@@ -1059,8 +1035,8 @@ where
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>>
@@ -1070,9 +1046,9 @@ pub fn render_app_async_stream_with_context<IV>(
where
IV: IntoView + 'static,
{
handle_response(additional_context, app_fn, |app, chunks, _supports_ooo| {
handle_response(additional_context, app_fn, |app, chunks| {
Box::pin(async move {
let app = if cfg!(feature = "islands-router") {
let app = if cfg!(feature = "dont-use-islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
@@ -1126,8 +1102,8 @@ where
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>>
@@ -1143,13 +1119,12 @@ where
fn async_stream_builder<IV>(
app: IV,
chunks: BoxedFnOnce<PinnedStream<String>>,
_supports_ooo: bool,
) -> PinnedFuture<PinnedStream<String>>
where
IV: IntoView + 'static,
{
Box::pin(async move {
let app = if cfg!(feature = "islands-router") {
let app = if cfg!(feature = "dont-use-islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
@@ -1422,7 +1397,6 @@ impl StaticRouteGenerator {
app_fn.clone(),
additional_context,
async_stream_builder,
false,
);
let sc = owner.shared_context().unwrap();
@@ -1668,7 +1642,7 @@ where
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;
@@ -1683,8 +1657,8 @@ where
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;
@@ -1717,15 +1691,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(_) => {
@@ -1757,7 +1728,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,
@@ -1773,8 +1744,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,
@@ -1855,64 +1826,64 @@ where
}
} else {
router.route(
path,
match listing.mode() {
SsrMode::OutOfOrder => {
let s = render_app_to_stream_with_context(
cx_with_state_and_method.clone(),
app_fn.clone(),
);
match method {
leptos_router::Method::Get => get(s),
leptos_router::Method::Post => post(s),
leptos_router::Method::Put => put(s),
leptos_router::Method::Delete => delete(s),
leptos_router::Method::Patch => patch(s),
}
path,
match listing.mode() {
SsrMode::OutOfOrder => {
let s = render_app_to_stream_with_context(
cx_with_state_and_method.clone(),
app_fn.clone(),
);
match method {
leptos_router::Method::Get => get(s),
leptos_router::Method::Post => post(s),
leptos_router::Method::Put => put(s),
leptos_router::Method::Delete => delete(s),
leptos_router::Method::Patch => patch(s),
}
SsrMode::PartiallyBlocked => {
let s = render_app_to_stream_with_context_and_replace_blocks(
cx_with_state_and_method.clone(),
app_fn.clone(),
true
);
match method {
leptos_router::Method::Get => get(s),
leptos_router::Method::Post => post(s),
leptos_router::Method::Put => put(s),
leptos_router::Method::Delete => delete(s),
leptos_router::Method::Patch => patch(s),
}
}
SsrMode::PartiallyBlocked => {
let s = render_app_to_stream_with_context_and_replace_blocks(
cx_with_state_and_method.clone(),
app_fn.clone(),
true
);
match method {
leptos_router::Method::Get => get(s),
leptos_router::Method::Post => post(s),
leptos_router::Method::Put => put(s),
leptos_router::Method::Delete => delete(s),
leptos_router::Method::Patch => patch(s),
}
SsrMode::InOrder => {
let s = render_app_to_stream_in_order_with_context(
cx_with_state_and_method.clone(),
app_fn.clone(),
);
match method {
leptos_router::Method::Get => get(s),
leptos_router::Method::Post => post(s),
leptos_router::Method::Put => put(s),
leptos_router::Method::Delete => delete(s),
leptos_router::Method::Patch => patch(s),
}
}
SsrMode::InOrder => {
let s = render_app_to_stream_in_order_with_context(
cx_with_state_and_method.clone(),
app_fn.clone(),
);
match method {
leptos_router::Method::Get => get(s),
leptos_router::Method::Post => post(s),
leptos_router::Method::Put => put(s),
leptos_router::Method::Delete => delete(s),
leptos_router::Method::Patch => patch(s),
}
SsrMode::Async => {
let s = render_app_async_with_context(
cx_with_state_and_method.clone(),
app_fn.clone(),
);
match method {
leptos_router::Method::Get => get(s),
leptos_router::Method::Post => post(s),
leptos_router::Method::Put => put(s),
leptos_router::Method::Delete => delete(s),
leptos_router::Method::Patch => patch(s),
}
}
SsrMode::Async => {
let s = render_app_async_with_context(
cx_with_state_and_method.clone(),
app_fn.clone(),
);
match method {
leptos_router::Method::Get => get(s),
leptos_router::Method::Post => post(s),
leptos_router::Method::Put => put(s),
leptos_router::Method::Delete => delete(s),
leptos_router::Method::Patch => patch(s),
}
_ => unreachable!()
},
)
}
_ => unreachable!()
},
)
};
}
}
@@ -2006,67 +1977,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_with_context<S, IV>(
additional_context: impl Fn() + 'static + Clone + Send,
shell: fn(LeptosOptions) -> IV,
) -> impl Fn(
Uri,
State<S>,
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
+ Clone
+ Send
+ 'static
where
IV: IntoView + 'static,
S: Send + Sync + Clone + 'static,
LeptosOptions: FromRef<S>,
{
move |uri: Uri, State(state): State<S>, req: Request<Body>| {
Box::pin({
let additional_context = additional_context.clone();
async move {
let options = LeptosOptions::from_ref(&state);
let res =
get_static_file(uri, &options.site_root, req.headers());
let res = res.await.unwrap();
if res.status() == StatusCode::OK {
res.into_response()
} else {
let mut res = handle_response_inner(
move || {
additional_context();
provide_context(state.clone());
},
move || shell(options),
req,
|app, chunks, _supports_ooo| {
Box::pin(async move {
let app = app
.to_html_stream_in_order()
.collect::<String>()
.await;
let chunks = chunks();
Box::pin(once(async move { app }).chain(chunks))
as PinnedStream<String>
})
},
)
.await;
*res.status_mut() = StatusCode::NOT_FOUND;
res
}
}
})
}
}
/// 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,
@@ -2084,10 +1994,40 @@ pub fn file_and_error_handler<S, IV>(
+ 'static
where
IV: IntoView + 'static,
S: Send + Sync + Clone + 'static,
S: Send + 'static,
LeptosOptions: FromRef<S>,
{
file_and_error_handler_with_context(move || (), shell)
move |uri: Uri, State(options): State<S>, req: Request<Body>| {
Box::pin(async move {
let options = LeptosOptions::from_ref(&options);
let res = get_static_file(uri, &options.site_root, req.headers());
let res = res.await.unwrap();
if res.status() == StatusCode::OK {
res.into_response()
} else {
let mut res = handle_response_inner(
|| {},
move || shell(options),
req,
|app, chunks| {
Box::pin(async move {
let app = app
.to_html_stream_in_order()
.collect::<String>()
.await;
let chunks = chunks();
Box::pin(once(async move { app }).chain(chunks))
as PinnedStream<String>
})
},
)
.await;
*res.status_mut() = StatusCode::NOT_FOUND;
res
}
})
}
}
#[cfg(feature = "default")]

View File

@@ -1,5 +1,3 @@
#![allow(clippy::type_complexity)]
use futures::{stream::once, Stream, StreamExt};
use hydration_context::{SharedContext, SsrSharedContext};
use leptos::{
@@ -33,20 +31,14 @@ pub trait ExtendResponse: Sized {
stream_builder: fn(
IV,
BoxedFnOnce<PinnedStream<String>>,
bool,
) -> PinnedFuture<PinnedStream<String>>,
supports_ooo: bool,
) -> impl Future<Output = Self> + Send
where
IV: IntoView + 'static,
{
async move {
let (owner, stream) = build_response(
app_fn,
additional_context,
stream_builder,
supports_ooo,
);
let (owner, stream) =
build_response(app_fn, additional_context, stream_builder);
let sc = owner.shared_context().unwrap();
@@ -102,11 +94,7 @@ pub fn build_response<IV>(
stream_builder: fn(
IV,
BoxedFnOnce<PinnedStream<String>>,
// this argument indicates whether a request wants to support out-of-order streaming
// responses
bool,
) -> PinnedFuture<PinnedStream<String>>,
is_islands_router_navigation: bool,
) -> (Owner, PinnedFuture<PinnedStream<String>>)
where
IV: IntoView + 'static,
@@ -150,7 +138,7 @@ where
//
// we also don't actually start hydrating until after the whole stream is complete,
// so it's not useful to send those scripts down earlier.
stream_builder(app, chunks, is_islands_router_navigation)
stream_builder(app, chunks)
});
stream.await

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,24 +28,24 @@ 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"
serde_json = { version = "1.0", optional = true }
server_fn = { workspace = true, features = ["form-redirects", "browser"] }
server_fn = { workspace = true, features = [
"form-redirects",
"browser",
"url",
] }
web-sys = { version = "0.3.72", features = [
"ShadowRoot",
"ShadowRootInit",
"ShadowRootMode",
] }
wasm-bindgen = { workspace = true }
wasm-bindgen = "0.2.95"
serde_qs = "0.13.0"
slotmap = "1.0"
futures = "0.3.31"
@@ -59,7 +56,7 @@ hydration = [
"reactive_graph/hydration",
"leptos_server/hydration",
"hydration_context/browser",
"leptos_dom/hydration",
"leptos_dom/hydration"
]
csr = ["leptos_macro/csr", "reactive_graph/effects"]
hydrate = [
@@ -78,7 +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",
@@ -92,19 +89,10 @@ spin = ["leptos-spin-macro"]
islands = ["leptos_macro/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"]
# Having an erasure feature rather than normal --cfg erase_components for the proc macro crate is a workaround for this rust issue:
# https://github.com/rust-lang/cargo/issues/4423
# TLDR proc macros will ignore RUSTFLAGS when --target is specified on the cargo command.
# This works around the issue by the non proc-macro crate which does see RUSTFLAGS enabling the replacement feature on the proc-macro crate, which wouldn't.
# This is automatic as long as the leptos crate is depended upon,
# downstream usage should never manually enable this feature.
[target.'cfg(erase_components)'.dependencies]
leptos_macro = { workspace = true, features = ["__internal_erase_components"] }
[package.metadata.cargo-all-features]
denylist = [
"nightly",
@@ -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",
]
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,167 +0,0 @@
use crate::attr::{
any_attribute::{AnyAttribute, IntoAnyAttribute},
Attribute, NextAttribute,
};
use leptos::prelude::*;
/// 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) -> Self::State {
self.children.build()
}
fn rebuild(self, state: &mut Self::State) {
self.children.rebuild(state);
}
}
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) {
self.children.dry_resolve()
}
fn resolve(
self,
) -> impl std::future::Future<Output = Self::AsyncOutput> + Send {
self.children.resolve()
}
fn to_html_with_buf(
self,
buf: &mut String,
position: &mut leptos::tachys::view::Position,
escape: bool,
mark_branches: bool,
_extra_attrs: Vec<AnyAttribute>,
) {
self.children.to_html_with_buf(
buf,
position,
escape,
mark_branches,
vec![],
)
}
fn hydrate<const FROM_SERVER: bool>(
self,
cursor: &leptos::tachys::hydration::Cursor,
position: &leptos::tachys::view::PositionState,
) -> Self::State {
self.children.hydrate::<FROM_SERVER>(cursor, position)
}
fn into_owned(self) -> Self::Owned {
AttributeInterceptorInner {
children_builder: self.children_builder,
children: self.children,
attributes: self.attributes.into_cloneable_owned(),
}
}
}

View File

@@ -43,20 +43,13 @@
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,26 +212,11 @@ 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 {}
@@ -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;
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;
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;
@@ -285,13 +285,6 @@ 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> {

View File

@@ -11,7 +11,7 @@ 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,
@@ -163,14 +163,6 @@ where
self.children.insert_before_this(child)
}
}
fn elements(&self) -> Vec<tachys::renderer::types::Element> {
if let Some(fallback) = &self.fallback {
fallback.elements()
} else {
self.children.elements()
}
}
}
impl<Chil, FalFn, Fal> Render for ErrorBoundaryView<Chil, FalFn>
@@ -276,7 +268,6 @@ where
Fal: RenderHtml + Send + 'static,
{
type AsyncOutput = ErrorBoundaryView<Chil::AsyncOutput, FalFn>;
type Owned = Self;
const MIN_LENGTH: usize = Chil::MIN_LENGTH;
@@ -310,7 +301,6 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) {
// first, attempt to serialize the children to HTML, then check for errors
let _hook = throw_error::set_error_hook(self.hook);
@@ -321,7 +311,6 @@ where
&mut new_pos,
escape,
mark_branches,
extra_attrs.clone(),
);
// any thrown errors would've been caught here
@@ -334,7 +323,6 @@ where
position,
escape,
mark_branches,
extra_attrs,
);
}
}
@@ -345,7 +333,6 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) where
Self: Sized,
{
@@ -358,7 +345,6 @@ where
&mut new_pos,
escape,
mark_branches,
extra_attrs.clone(),
);
// any thrown errors would've been caught here
@@ -372,7 +358,6 @@ where
position,
escape,
mark_branches,
extra_attrs,
);
buf.push_sync(&fallback);
}
@@ -438,10 +423,6 @@ where
},
)
}
fn into_owned(self) -> Self::Owned {
self
}
}
#[derive(Debug)]

View File

@@ -6,10 +6,7 @@ use reactive_graph::{
traits::Set,
};
use std::hash::Hash;
use tachys::{
reactive_graph::OwnedView,
view::keyed::{keyed, SerializableKey},
};
use tachys::{reactive_graph::OwnedView, view::keyed::keyed};
/// Iterates over children and displays them, keyed by the `key` function given.
///
@@ -47,67 +44,6 @@ use tachys::{
/// }
/// }
/// ```
///
/// 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>(
@@ -124,7 +60,7 @@ where
EF: Fn(T) -> N + Send + Clone + 'static,
N: IntoView + 'static,
KF: Fn(&T) -> K + Send + Clone + 'static,
K: Eq + Hash + SerializableKey + 'static,
K: Eq + Hash + 'static,
T: Send + 'static,
{
// this takes the owner of the For itself
@@ -198,7 +134,7 @@ where
EF: Fn(ReadSignal<usize>, T) -> N + Send + Clone + 'static,
N: IntoView + 'static,
KF: Fn(&T) -> K + Send + Clone + 'static,
K: Eq + Hash + SerializableKey + 'static,
K: Eq + Hash + 'static,
T: Send + 'static,
{
// this takes the owner of the For itself
@@ -221,7 +157,6 @@ where
};
move || keyed(each(), key.clone(), children.clone())
}
/*
#[cfg(test)]
mod tests {

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,
Http, ServerFn,
client::Client, codec::PostUrl, request::ClientReq, ServerFn, ServerFnError,
};
use tachys::{
either::Either,
@@ -75,7 +71,7 @@ use web_sys::{
/// ```
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
#[component]
pub fn ActionForm<ServFn, OutputProtocol>(
pub fn ActionForm<ServFn>(
/// The action from which to build the form.
action: ServerAction<ServFn>,
/// A [`NodeRef`] in which the `<form>` element should be stored.
@@ -86,7 +82,7 @@ pub fn ActionForm<ServFn, OutputProtocol>(
) -> impl IntoView
where
ServFn: DeserializeOwned
+ ServerFn<Protocol = Http<PostUrl, OutputProtocol>>
+ ServerFn<InputEncoding = PostUrl>
+ Clone
+ Send
+ Sync
@@ -125,10 +121,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);
}
}
@@ -151,7 +146,7 @@ where
/// [`form`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form)
/// progressively enhanced to use client-side routing.
#[component]
pub fn MultiActionForm<ServFn, OutputProtocol>(
pub fn MultiActionForm<ServFn>(
/// The action from which to build the form.
action: ServerMultiAction<ServFn>,
/// A [`NodeRef`] in which the `<form>` element should be stored.
@@ -165,7 +160,7 @@ where
+ Sync
+ Clone
+ DeserializeOwned
+ ServerFn<Protocol = Http<PostUrl, OutputProtocol>>
+ ServerFn<InputEncoding = PostUrl>
+ 'static,
ServFn::Output: Send + Sync + 'static,
<<ServFn::Client as Client<ServFn::Error>>::Request as ClientReq<
@@ -192,10 +187,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()));
)));
}
}
};

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,52 +6,56 @@
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);
});
window.__hydrateIsland = (el, id) => hydrateIsland(el, id, mod);
})
});
})

View File

@@ -1,378 +0,0 @@
let NAVIGATION = 0;
window.addEventListener("click", async (ev) => {
const req = clickToReq(ev);
if(!req) {
return;
}
ev.preventDefault();
await navigateToPage(req, true);
});
window.addEventListener("popstate", async (ev) => {
const req = new Request(window.location);
ev.preventDefault();
await navigateToPage(req, true, true);
});
window.addEventListener("submit", async (ev) => {
const req = submitToReq(ev);
if(!req) {
return;
}
ev.preventDefault();
await navigateToPage(req, true);
});
async function navigateToPage(
/** @type Request */
req,
/** @type bool */
useViewTransition,
/** @type bool */
replace
) {
NAVIGATION += 1;
const currentNav = NAVIGATION;
// add a custom header to indicate that we're on a subsequent navigation
req.headers.append("Islands-Router", "true");
// fetch the new page
const resp = await fetch(req);
const redirected = resp.redirected;
const htmlString = await resp.text();
if(NAVIGATION === currentNav) {
// The 'doc' variable now contains the parsed DOM
const transition = async () => {
try {
diffPages(htmlString);
for(const island of document.querySelectorAll("leptos-island")) {
if(!island.$$hydrated) {
__hydrateIsland(island, island.dataset.component);
island.$$hydrated = true;
}
}
} catch(e) {
console.error(e);
}
};
// Not all browsers support startViewTransition; see https://caniuse.com/?search=startViewTransition
if (useViewTransition && document.startViewTransition) {
await document.startViewTransition(transition);
} else {
await transition()
}
const url = redirected ? resp.url : req.url;
if(replace) {
window.history.replaceState(undefined, null, url);
} else {
window.history.pushState(undefined, null, url);
}
}
}
function clickToReq(ev) {
// confirm that this is an <a> that meets our requirements
if (
ev.defaultPrevented ||
ev.button !== 0 ||
ev.metaKey ||
ev.altKey ||
ev.ctrlKey ||
ev.shiftKey
)
return;
/** @type HTMLAnchorElement | undefined;*/
const a = ev
.composedPath()
.find(el => el instanceof Node && el.nodeName.toUpperCase() === "A");
if (!a) return;
const svg = a.namespaceURI === "http://www.w3.org/2000/svg";
const href = svg ? a.href.baseVal : a.href;
const target = svg ? a.target.baseVal : a.target;
if (target || (!href && !a.hasAttribute("state"))) return;
const rel = (a.getAttribute("rel") || "").split(/\s+/);
if (a.hasAttribute("download") || (rel?.includes("external"))) return;
const url = svg ? new URL(href, document.baseURI) : new URL(href);
if (
url.origin !== window.location.origin // ||
// TODO base
//(basePath && url.pathname && !url.pathname.toLowerCase().startsWith(basePath.toLowerCase()))
)
return;
return new Request(url);
}
function submitToReq(ev) {
event.preventDefault();
const target = ev.target;
/** @type HTMLFormElement */
let form;
if(target instanceof HTMLFormElement) {
form = target;
} else {
if(!target.form) {
return;
}
form = target.form;
}
const method = form.method.toUpperCase();
if(method !== "GET" && method !== "POST") {
return;
}
const url = new URL(form.action);
let path = url.pathname;
const requestInit = {};
const data = new FormData(form);
const params = new URLSearchParams();
for (const [key, value] of data.entries()) {
params.append(key, value);
}
requestInit.headers = {
Accept: "text/html"
};
if(method === "GET") {
path += `?${params.toString()}`;
}
else {
requestInit.method = "POST";
requestInit.body = params;
}
return new Request(
path,
requestInit
);
}
function diffPages(htmlString) {
// Use DOMParser to parse the HTML string
const parser = new DOMParser();
const doc = parser.parseFromString(htmlString, 'text/html');
diffRange(document, document, doc, doc);
}
function diffRange(oldDocument, oldRoot, newDocument, newRoot, oldEnd, newEnd) {
const oldDocWalker = oldDocument.createTreeWalker(oldRoot);
const newDocWalker = newDocument.createTreeWalker(newRoot);
let oldNode = oldDocWalker.currentNode;
let newNode = newDocWalker.currentNode;
while (oldDocWalker.nextNode() && newDocWalker.nextNode()) {
oldNode = oldDocWalker.currentNode;
newNode = newDocWalker.currentNode;
if (oldNode == oldEnd || newNode == newEnd) {
break;
}
// if the nodes are different, we need to replace the old with the new
// because of the typed view tree, this should never actually happen
if (oldNode.nodeType !== newNode.nodeType) {
oldNode.replaceWith(newNode);
}
// if it's a text node, just update the text with the new text
else if (oldNode.nodeType === Node.TEXT_NODE) {
oldNode.textContent = newNode.textContent;
}
// if it's an element, replace if it's a different tag, or update attributes
else if (oldNode.nodeType === Node.ELEMENT_NODE) {
diffElement(oldNode, newNode);
}
// we use comment "branch marker" nodes to distinguish between different branches in the statically-typed view tree
// if one of these marker is hit, then there are two options
// 1) it's the same branch, and we just keep walking until the end
// 2) it's a different branch, in which case the old can be replaced with the new wholesale
else if (oldNode.nodeType === Node.COMMENT_NODE) {
const oldText = oldNode.textContent;
const newText = newNode.textContent;
if(oldText.startsWith("bo-for")) {
replaceFor(oldDocument, oldDocWalker, newDocument, newDocWalker, oldNode, newNode);
}
else if (oldText.startsWith("bo-item")) {
// skip, this means we're diffing a new item within a For
}
else if(oldText.startsWith("bo") && newText !== oldText) {
replaceBranch(oldDocWalker, newDocWalker, oldNode, newNode);
}
}
}
}
function replaceFor(oldDocument, oldDocWalker, newDocument, newDocWalker, oldNode, newNode) {
oldDocWalker.nextNode();
newDocWalker.nextNode();
const oldRange = new Range();
const newRange = new Range();
let oldBranches = 1;
let newBranches = 1;
const oldKeys = {};
const newKeys = {};
while(oldBranches > 0) {
const c = oldDocWalker.currentNode;
if(c.nodeType === Node.COMMENT_NODE) {
const t = c.textContent;
if(t.startsWith("bo-for")) {
oldBranches += 1;
} else if(t.startsWith("bc-for")) {
oldBranches -= 1;
} else if (t.startsWith("bo-item")) {
const k = t.replace("bo-item-", "");
oldKeys[k] = { open: c, close: null };
} else if (t.startsWith("bc-item")) {
const k = t.replace("bc-item-", "");
oldKeys[k].close = c;
}
}
oldDocWalker.nextNode();
}
while(newBranches > 0) {
const c = newDocWalker.currentNode;
if(c.nodeType === Node.COMMENT_NODE) {
const t = c.textContent;
if(t.startsWith("bo-for")) {
newBranches += 1;
} else if(t.startsWith("bc-for")) {
newBranches -= 1;
} else if (t.startsWith("bo-item")) {
const k = t.replace("bo-item-", "");
newKeys[k] = { open: c, close: null };
} else if (t.startsWith("bc-item")) {
const k = t.replace("bc-item-", "");
newKeys[k].close = c;
}
}
newDocWalker.nextNode();
}
for(const key in oldKeys) {
if(newKeys[key]) {
const oldOne = oldKeys[key];
const newOne = newKeys[key];
const oldRange = new Range();
const newRange = new Range();
// then replace the item in the *new* list with the *old* DOM elements
oldRange.setStartAfter(oldOne.open);
oldRange.setEndBefore(oldOne.close);
newRange.setStartAfter(newOne.open);
newRange.setEndBefore(newOne.close);
const oldContents = oldRange.extractContents();
const newContents = newRange.extractContents();
// patch the *old* DOM elements with the new ones
diffRange(oldDocument, oldContents, newDocument, newContents, oldOne.close, newOne.close);
// then insert the old DOM elements into the new tree
// this means you'll end up with any new attributes or content from the server,
// but with any old DOM state (because they are the old elements)
newRange.insertNode(oldContents);
newOne.open.replaceWith(oldOne.open);
newOne.close.replaceWith(oldOne.close);
}
}
try {
oldRange.setStartAfter(oldNode);
oldRange.setEndBefore(oldDocWalker.currentNode);
newRange.setStartAfter(newNode);
newRange.setEndAfter(newDocWalker.currentNode);
const newContents = newRange.extractContents();
oldRange.deleteContents();
oldRange.insertNode(newContents);
oldNode.replaceWith(newNode);
oldDocWalker.currentNode.replaceWith(newDocWalker.currentNode);
} catch (e) {
console.error(e);
}
}
function replaceBranch(oldDocWalker, newDocWalker, oldNode, newNode) {
oldDocWalker.nextNode();
newDocWalker.nextNode();
const oldRange = new Range();
const newRange = new Range();
let oldBranches = 1;
let newBranches = 1;
while(oldBranches > 0) {
if(oldDocWalker.nextNode()) {
if(oldDocWalker.currentNode.nodeType === Node.COMMENT_NODE) {
if(oldDocWalker.currentNode.textContent.startsWith("bo")) {
oldBranches += 1;
} else if(oldDocWalker.currentNode.textContent.startsWith("bc")) {
oldBranches -= 1;
}
}
}
}
while(newBranches > 0) {
if(newDocWalker.nextNode()) {
if(newDocWalker.currentNode.nodeType === Node.COMMENT_NODE) {
if(newDocWalker.currentNode.textContent.startsWith("bo")) {
newBranches += 1;
} else if(newDocWalker.currentNode.textContent.startsWith("bc")) {
newBranches -= 1;
}
}
}
}
try {
oldRange.setStartAfter(oldNode);
oldRange.setEndBefore(oldDocWalker.currentNode);
newRange.setStartAfter(newNode);
newRange.setEndAfter(newDocWalker.currentNode);
const newContents = newRange.extractContents();
oldRange.deleteContents();
oldRange.insertNode(newContents);
oldNode.replaceWith(newNode);
oldDocWalker.currentNode.replaceWith(newDocWalker.currentNode);
} catch (e) {
console.error(e);
}
}
function diffElement(oldNode, newNode) {
/** @type Element */
const oldEl = oldNode;
/** @type Element */
const newEl = newNode;
if (oldEl.tagName !== newEl.tagName) {
oldEl.replaceWith(newEl);
}
else {
for(const attr of newEl.attributes) {
oldEl.setAttribute(attr.name, attr.value);
}
}
}
for(const island of document.querySelectorAll("leptos-island")) {
island.$$hydrated = true;
}

View File

@@ -50,10 +50,6 @@ pub fn HydrationScripts(
/// Should be `true` to hydrate in `islands` mode.
#[prop(optional)]
islands: bool,
/// Should be `true` to add the “islands router,” which enables limited client-side routing
/// when running in islands mode.
#[prop(optional)]
islands_router: bool,
/// A base url, not including a trailing slash
#[prop(optional, into)]
root: Option<String>,
@@ -102,36 +98,18 @@ pub fn HydrationScripts(
include_str!("./hydration_script.js")
};
let islands_router = islands_router
.then_some(include_str!("./islands_routing.js"))
.unwrap_or_default();
let root = root.unwrap_or_default();
use_context::<IslandsRouterNavigation>().is_none().then(|| {
view! {
<link rel="modulepreload" href=format!("{root}/{pkg_path}/{js_file_name}.js") nonce=nonce.clone()/>
<link
rel="preload"
href=format!("{root}/{pkg_path}/{wasm_file_name}.wasm")
r#as="fetch"
r#type="application/wasm"
crossorigin=nonce.clone().unwrap_or_default()
/>
<script type="module" nonce=nonce>
{format!("{script}({root:?}, {pkg_path:?}, {js_file_name:?}, {wasm_file_name:?});{islands_router}")}
</script>
}
})
view! {
<link rel="modulepreload" href=format!("{root}/{pkg_path}/{js_file_name}.js") nonce=nonce.clone()/>
<link
rel="preload"
href=format!("{root}/{pkg_path}/{wasm_file_name}.wasm")
r#as="fetch"
r#type="application/wasm"
crossorigin=nonce.clone().unwrap_or_default()
/>
<script type="module" nonce=nonce>
{format!("{script}({root:?}, {pkg_path:?}, {js_file_name:?}, {wasm_file_name:?})")}
</script>
}
}
/// If this is provided via context, it means that you are using the islands router and
/// this is a subsequent navigation, made from the client.
///
/// This should be provided automatically by a server integration if it detects that the
/// header `Islands-Router` is present in the request.
///
/// This is used to determine how much of the hydration script to include in the page.
/// If it is present, then the contents of the `<HydrationScripts>` component will not be
/// included, as they only need to be sent to the client once.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IslandsRouterNavigation;

View File

@@ -1,6 +1,6 @@
use std::borrow::Cow;
use tachys::{
html::attribute::{any_attribute::AnyAttribute, Attribute},
html::attribute::Attribute,
hydration::Cursor,
ssr::StreamBuilder,
view::{
@@ -87,7 +87,6 @@ impl<T: Render> Render for View<T> {
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;
@@ -105,7 +104,6 @@ impl<T: RenderHtml> RenderHtml for View<T> {
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) {
#[cfg(debug_assertions)]
let vm = self.view_marker.to_owned();
@@ -114,13 +112,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() {
@@ -134,7 +127,6 @@ impl<T: RenderHtml> RenderHtml for View<T> {
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) where
Self: Sized,
{
@@ -150,7 +142,6 @@ impl<T: RenderHtml> RenderHtml for View<T> {
position,
escape,
mark_branches,
extra_attrs,
);
#[cfg(debug_assertions)]
@@ -166,14 +157,6 @@ impl<T: RenderHtml> RenderHtml for View<T> {
) -> Self::State {
self.inner.hydrate::<FROM_SERVER>(cursor, position)
}
fn into_owned(self) -> Self::Owned {
View {
inner: self.inner.into_owned(),
#[cfg(debug_assertions)]
view_marker: self.view_marker,
}
}
}
impl<T: ToTemplate> ToTemplate for View<T> {

View File

@@ -172,10 +172,12 @@ 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::{fragment::Fragment, template::ViewTemplate},
view::{
any_view::AnyView, fragment::Fragment, template::ViewTemplate,
},
};
}
pub use export_types::*;
@@ -190,9 +192,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,7 +290,7 @@ pub mod logging {
/// 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`].

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,10 +16,9 @@ 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,
@@ -133,18 +132,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>,
@@ -247,7 +234,6 @@ 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;
@@ -263,15 +249,9 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: 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>(
@@ -280,7 +260,6 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) where
Self: Sized,
{
@@ -311,13 +290,11 @@ where
let eff = reactive_graph::effect::Effect::new_isomorphic({
move |_| {
tasks.track();
if let Some(tasks) = tasks.try_read() {
if tasks.is_empty() {
if let Some(tx) = tasks_tx.take() {
// If the receiver has dropped, it means the ScopedFuture has already
// dropped, so it doesn't matter if we manage to send this.
_ = tx.send(());
}
if tasks.read().is_empty() {
if let Some(tx) = tasks_tx.take() {
// If the receiver has dropped, it means the ScopedFuture has already
// dropped, so it doesn't matter if we manage to send this.
_ = tx.send(());
}
}
}
@@ -379,7 +356,6 @@ where
position,
escape,
mark_branches,
extra_attrs,
);
}
Some(None) => {
@@ -389,7 +365,6 @@ where
position,
escape,
mark_branches,
extra_attrs,
);
}
None => {
@@ -403,15 +378,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;
@@ -426,7 +394,6 @@ where
&mut position,
escape,
mark_branches,
extra_attrs,
);
builder.finish().take_chunks()
}
@@ -476,10 +443,6 @@ where
}
})
}
fn into_owned(self) -> Self::Owned {
self
}
}
/// A wrapper that prevents [`Suspense`] from waiting for any resource reads that happen inside
@@ -532,7 +495,6 @@ where
T: RenderHtml + 'static,
{
type AsyncOutput = Self;
type Owned = Self;
const MIN_LENGTH: usize = T::MIN_LENGTH;
@@ -548,15 +510,8 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: 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>(
@@ -565,7 +520,6 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) where
Self: Sized,
{
@@ -574,7 +528,6 @@ where
position,
escape,
mark_branches,
extra_attrs,
);
}
@@ -585,8 +538,4 @@ where
) -> Self::State {
(self.0)().hydrate::<FROM_SERVER>(cursor, position)
}
fn into_owned(self) -> Self::Owned {
self
}
}

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 = Arc<dyn Fn() -> Oco<'static, str> + Send + Sync>;
fn into_attribute_value(self) -> Self::Output {
self.0
}
}

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,
})
}

View File

@@ -1535,7 +1535,7 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.7.2"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
dependencies = [
@@ -1851,7 +1851,7 @@ dependencies = [
[[package]]
name = "quote-use"
version = "0.7.2"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58e9a38ef862d7fec635661503289062bc5b3035e61859a8de3d3f81823accd2"
dependencies = [
@@ -1953,7 +1953,7 @@ dependencies = [
[[package]]
name = "ron"
version = "0.7.2"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a"
dependencies = [
@@ -2104,7 +2104,7 @@ dependencies = [
[[package]]
name = "serde_urlencoded"
version = "0.7.2"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [

View File

@@ -10,7 +10,7 @@ rust-version.workspace = true
edition.workspace = true
[dependencies]
config = { version = "0.15.8", default-features = false, features = [
config = { version = "0.14.1", default-features = false, features = [
"toml",
"convert-case",
] }
@@ -20,12 +20,9 @@ thiserror = "2.0"
typed-builder = "0.20.0"
[dev-dependencies]
tokio = { version = "1.43", features = ["rt", "macros"] }
tokio = { version = "1.41", features = ["rt", "macros"] }
tempfile = "3.14"
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

@@ -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 = { workspace = true }
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 = { workspace = true }
version = "0.7.0-rc2"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
@@ -16,7 +16,7 @@ proc-macro = true
attribute-derive = { version = "0.10.3", features = ["syn-full"] }
cfg-if = "1.0"
html-escape = "0.2.13"
itertools = { workspace = true }
itertools = "0.13.0"
prettyplease = "0.2.25"
proc-macro-error2 = { version = "2.0", default-features = false }
proc-macro2 = "1.0"
@@ -25,16 +25,15 @@ syn = { version = "2.0", features = ["full"] }
rstml = "0.12.0"
leptos_hot_reload = { workspace = true }
server_fn_macro = { workspace = true }
convert_case = "0.7"
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"
@@ -46,47 +45,42 @@ ssr = ["server_fn_macro/ssr", "leptos/ssr"]
nightly = ["server_fn_macro/nightly"]
tracing = ["dep:tracing"]
islands = []
trace-components = []
trace-component-props = []
actix = ["server_fn_macro/actix"]
axum = ["server_fn_macro/axum"]
generic = ["server_fn_macro/generic"]
# Having an erasure feature rather than normal --cfg erase_components for the proc macro crate is a workaround for this rust issue:
# https://github.com/rust-lang/cargo/issues/4423
# TLDR proc macros will ignore RUSTFLAGS when --target is specified on the cargo command.
# This works around the issue by the non proc-macro crate which does see RUSTFLAGS enabling the replacement feature on the proc-macro crate, which wouldn't.
# This is automatic as long as the leptos crate is depended upon,
# downstream usage should never manually enable this feature.
__internal_erase_components = []
[package.metadata.cargo-all-features]
denylist = ["nightly", "tracing", "trace-component-props"]
skip_feature_sets = [
[
"csr",
"hydrate",
],
[
"hydrate",
"csr",
],
[
"hydrate",
"ssr",
],
[
"actix",
"axum",
],
[
"actix",
"generic",
],
[
"generic",
"axum",
],
[
"csr",
"hydrate",
],
[
"hydrate",
"csr",
],
[
"hydrate",
"ssr",
],
[
"actix",
"axum",
],
[
"actix",
"generic",
],
[
"generic",
"axum",
],
]
[package.metadata.docs.rs]
rustdoc-args = ["--generate-link-to-definition"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(erase_components)'] }

View File

@@ -11,13 +11,13 @@ dependencies = [
[tasks.test-leptos_macro-example]
description = "Tests the leptos_macro/example to check if macro handles doc comments correctly"
command = "cargo"
args = ["+nightly-2025-03-05", "test", "--doc"]
args = ["+nightly-2024-08-01", "test", "--doc"]
cwd = "example"
install_crate = false
[tasks.doc-leptos_macro-example]
description = "Docs the leptos_macro/example to check if macro handles doc comments correctly"
command = "cargo"
args = ["+nightly-2025-03-05", "doc"]
args = ["+nightly-2024-08-01", "doc"]
cwd = "example"
install_crate = false

View File

@@ -32,8 +32,6 @@ pub struct Model {
impl Parse for Model {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut item = ItemFn::parse(input)?;
maybe_modify_return_type(&mut item.sig.output);
convert_impl_trait_to_generic(&mut item.sig);
let docs = Docs::new(&item.attrs);
@@ -78,39 +76,6 @@ impl Parse for Model {
}
}
/// Exists to fix nested routes defined in a separate component in erased mode,
/// by replacing the return type with AnyNestedRoute, which is what it'll be, but is required as the return type for compiler inference.
fn maybe_modify_return_type(ret: &mut ReturnType) {
#[cfg(feature = "__internal_erase_components")]
{
if let ReturnType::Type(_, ty) = ret {
if let Type::ImplTrait(TypeImplTrait { bounds, .. }) = ty.as_ref() {
// If one of the bounds is MatchNestedRoutes, we need to replace the return type with AnyNestedRoute:
if bounds.iter().any(|bound| {
if let syn::TypeParamBound::Trait(trait_bound) = bound {
if trait_bound.path.segments.iter().any(
|path_segment| {
path_segment.ident == "MatchNestedRoutes"
},
) {
return true;
}
}
false
}) {
*ty = parse_quote!(
::leptos_router::any_nested_route::AnyNestedRoute
);
}
}
}
}
#[cfg(not(feature = "__internal_erase_components"))]
{
let _ = ret;
}
}
// implemented manually because Vec::drain_filter is nightly only
// follows std recommended parallel
pub fn drain_filter<T>(
@@ -179,6 +144,8 @@ impl ToTokens for Model {
let (impl_generics, generics, where_clause) =
body.sig.generics.split_for_impl();
let lifetimes = body.sig.generics.lifetimes();
let props_name = format_ident!("{name}Props");
let props_builder_name = format_ident!("{name}PropsBuilder");
let props_serialized_name = format_ident!("{name}PropsSerialized");
@@ -234,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();
@@ -315,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 {
@@ -331,10 +272,10 @@ impl ToTokens for Model {
let component = if *is_transparent {
body_expr
} else if cfg!(feature = "__internal_erase_components") {
} else if cfg!(erase_components) {
quote! {
::leptos::prelude::IntoMaybeErased::into_maybe_erased(
::leptos::reactive::graph::untrack_with_diagnostics(
::leptos::prelude::IntoAny::into_any(
::leptos::prelude::untrack(
move || {
#tracing_guard_expr
#tracing_props_expr
@@ -345,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
@@ -360,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
@@ -398,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
});
@@ -497,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! {}
@@ -603,7 +529,7 @@ impl ToTokens for Model {
#tracing_instrument_attr
#vis fn #name #impl_generics (
#props_arg
) #ret
) #ret #(+ #lifetimes)*
#where_clause
{
#body
@@ -648,8 +574,7 @@ impl Parse for DummyModel {
drain_filter(&mut attrs, |attr| !attr.path().is_ident("doc"));
let vis: Visibility = input.parse()?;
let mut sig: Signature = input.parse()?;
maybe_modify_return_type(&mut sig.output);
let sig: Signature = input.parse()?;
// The body is left untouched, so it will not cause an error
// even if the syntax is invalid.
@@ -730,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 {
@@ -970,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

@@ -23,7 +23,6 @@ mod params;
mod view;
use crate::component::unmodified_fn_name_from_fn_name;
mod component;
mod lazy;
mod memo;
mod slice;
mod slot;
@@ -281,11 +280,7 @@ pub fn view(tokens: TokenStream) -> TokenStream {
#[proc_macro]
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
pub fn template(tokens: TokenStream) -> TokenStream {
if cfg!(feature = "__internal_erase_components") {
view(tokens)
} else {
view_macro_impl(tokens, true)
}
view_macro_impl(tokens, true)
}
fn view_macro_impl(tokens: TokenStream, template: bool) -> TokenStream {
@@ -644,7 +639,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]`"
);
}
@@ -681,21 +676,17 @@ fn component_macro(
#[allow(non_snake_case, dead_code, clippy::too_many_arguments, clippy::needless_lifetimes)]
#unexpanded
}
} else {
match dummy {
Ok(mut dummy) => {
dummy.sig.ident = unmodified_fn_name_from_fn_name(&dummy.sig.ident);
quote! {
#[doc(hidden)]
#[allow(non_snake_case, dead_code, clippy::too_many_arguments, clippy::needless_lifetimes)]
#dummy
}
}
Err(e) => {
proc_macro_error2::abort!(e.span(), e);
}
} else if let Ok(mut dummy) = dummy {
dummy.sig.ident = unmodified_fn_name_from_fn_name(&dummy.sig.ident);
quote! {
#[doc(hidden)]
#[allow(non_snake_case, dead_code, clippy::too_many_arguments, clippy::needless_lifetimes)]
#dummy
}
}.into()
} else {
quote! {}
}
.into()
}
/// Annotates a struct so that it can be used with your Component as a `slot`.
@@ -927,7 +918,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,
) {
@@ -1011,17 +1002,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;
}
@@ -170,14 +167,8 @@ pub(crate) fn component_to_tokens(
.collect::<Vec<_>>();
let spreads = (!(spreads.is_empty())).then(|| {
if cfg!(feature = "__internal_erase_components") {
quote! {
.add_any_attr(vec![#(#spreads.into_any_attr(),)*])
}
} else {
quote! {
.add_any_attr((#(#spreads,)*))
}
quote! {
.add_any_attr((#(#spreads,)*))
}
});

View File

@@ -154,12 +154,7 @@ fn is_inert_element(orig_node: &Node<impl CustomNode>) -> bool {
Some(value) => {
matches!(&value.value, KVAttributeValue::Expr(expr) if {
if let Expr::Lit(lit) = expr {
let key = attr.key.to_string();
if key.starts_with("style:") || key.starts_with("prop:") || key.starts_with("on:") || key.starts_with("use:") || key.starts_with("bind") {
false
} else {
matches!(&lit.lit, Lit::Str(_))
}
matches!(&lit.lit, Lit::Str(_))
} else {
false
}
@@ -428,12 +423,6 @@ fn element_children_to_tokens(
{ #child }
)
})
} else if cfg!(feature = "__internal_erase_components") {
Some(quote! {
.child(
leptos::tachys::view::iterators::StaticVec::from(vec![#(#children.into_maybe_erased()),*])
)
})
} else if children.len() > 16 {
// implementations of various traits used in routing and rendering are implemented for
// tuples of sizes 0, 1, 2, 3, ... N. N varies but is > 16. The traits are also implemented
@@ -479,10 +468,6 @@ fn fragment_to_tokens(
None
} else if children.len() == 1 {
children.into_iter().next()
} else if cfg!(feature = "__internal_erase_components") {
Some(quote! {
leptos::tachys::view::iterators::StaticVec::from(vec![#(#children.into_maybe_erased()),*])
})
} else if children.len() > 16 {
// implementations of various traits used in routing and rendering are implemented for
// tuples of sizes 0, 1, 2, 3, ... N. N varies but is > 16. The traits are also implemented
@@ -667,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,18 +740,10 @@ pub(crate) fn element_to_tokens(
}
}
}
if cfg!(feature = "__internal_erase_components") {
Some(quote! {
vec![#(#attributes.into_any_attr(),)*]
#(.add_any_attr(#additions))*
})
} else {
Some(quote! {
(#(#attributes,)*)
#(.add_any_attr(#additions))*
})
}
Some(quote! {
(#(#attributes,)*)
#(.add_any_attr(#additions))*
})
} else {
let tag = name.to_string();
// collect close_tag name to emit semantic information for IDE.
@@ -790,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_" {
@@ -798,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
@@ -1152,11 +1117,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)
@@ -1175,14 +1135,8 @@ pub(crate) fn two_way_binding_to_tokens(
let ident =
format_ident!("{}", name.to_case(UpperCamel), span = node.key.span());
if name == "group" {
quote! {
.bind(leptos::tachys::reactive_graph::bind::#ident, #value)
}
} else {
quote! {
.bind(::leptos::attr::#ident, #value)
}
quote! {
.bind(::leptos::attr::#ident, #value)
}
}
@@ -1203,7 +1157,8 @@ pub(crate) fn event_type_and_handler(
) -> (TokenStream, TokenStream, TokenStream) {
let handler = attribute_value(node, false);
let (event_type, is_custom, options) = parse_event_name(name);
let (event_type, is_custom, is_force_undelegated, is_targeted) =
parse_event_name(name);
let event_name_ident = match &node.key {
NodeName::Punctuated(parts) => {
@@ -1221,17 +1176,11 @@ pub(crate) fn event_type_and_handler(
}
_ => unreachable!(),
};
let capture_ident = match &node.key {
NodeName::Punctuated(parts) => {
parts.iter().find(|part| part.to_string() == "capture")
}
_ => unreachable!(),
};
let on = match &node.key {
NodeName::Punctuated(parts) => &parts[0],
_ => unreachable!(),
};
let on = if options.targeted {
let on = if is_targeted {
Ident::new("on_target", on.span()).to_token_stream()
} else {
on.to_token_stream()
@@ -1244,29 +1193,15 @@ pub(crate) fn event_type_and_handler(
event_type
};
let event_type = quote! {
::leptos::tachys::html::event::#event_type
};
let event_type = if options.captured {
let capture = if let Some(capture) = capture_ident {
quote! { #capture }
} else {
quote! { capture }
};
quote! { ::leptos::tachys::html::event::#capture(#event_type) }
} else {
event_type
};
let event_type = if options.undelegated {
let event_type = if is_force_undelegated {
let undelegated = if let Some(undelegated) = undelegated_ident {
quote! { #undelegated }
} else {
quote! { undelegated }
};
quote! { ::leptos::tachys::html::event::#undelegated(#event_type) }
quote! { ::leptos::tachys::html::event::#undelegated(::leptos::tachys::html::event::#event_type) }
} else {
event_type
quote! { ::leptos::tachys::html::event::#event_type }
};
(on, event_type, handler)
@@ -1472,22 +1407,13 @@ fn is_ambiguous_element(tag: &str) -> bool {
tag == "a" || tag == "script" || tag == "title"
}
fn parse_event(event_name: &str) -> (String, EventNameOptions) {
let undelegated = event_name.contains(":undelegated");
let targeted = event_name.contains(":target");
let captured = event_name.contains(":capture");
fn parse_event(event_name: &str) -> (String, bool, bool) {
let is_undelegated = event_name.contains(":undelegated");
let is_targeted = event_name.contains(":target");
let event_name = event_name
.replace(":undelegated", "")
.replace(":target", "")
.replace(":capture", "");
(
event_name,
EventNameOptions {
undelegated,
targeted,
captured,
},
)
.replace(":target", "");
(event_name, is_undelegated, is_targeted)
}
/// Escapes Rust keywords that are also HTML attribute names
@@ -1679,17 +1605,8 @@ const TYPED_EVENTS: [&str; 126] = [
const CUSTOM_EVENT: &str = "Custom";
#[derive(Debug)]
pub(crate) struct EventNameOptions {
undelegated: bool,
targeted: bool,
captured: bool,
}
pub(crate) fn parse_event_name(
name: &str,
) -> (TokenStream, bool, EventNameOptions) {
let (name, options) = parse_event(name);
pub(crate) fn parse_event_name(name: &str) -> (TokenStream, bool, bool, bool) {
let (name, is_force_undelegated, is_targeted) = parse_event(name);
let (event_type, is_custom) = TYPED_EVENTS
.binary_search(&name.as_str())
@@ -1705,7 +1622,7 @@ pub(crate) fn parse_event_name(
} else {
event_type
};
(event_type, is_custom, options)
(event_type, is_custom, is_force_undelegated, is_targeted)
}
fn convert_to_snake_case(name: String) -> String {
@@ -1722,7 +1639,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(_) => {
@@ -1793,7 +1710,7 @@ fn tuple_name(name: &str, node: &KeyedAttribute) -> TupleName {
TupleName::None
}
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug)]
enum TupleName {
None,
Str(String),

View File

@@ -83,7 +83,7 @@ pub(crate) fn slot_to_tokens(
let value = attr.value().map(|v| {
quote! { #v }
})?;
Some(quote! { (#name, #value) })
Some(quote! { (#name, ::leptos::IntoAttribute::into_attribute(#value)) })
})
.collect::<Vec<_>>();

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,41 +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
/>
};
}
#[component]
fn WithLifetime<'a>(data: &'a str) -> impl IntoView {
_ = data;
"static lifetime"
}
#[test]
fn returns_static_lifetime() {
#[allow(unused)]
fn can_return_impl_intoview_from_body() -> impl IntoView {
let val = String::from("non_static_lifetime");
WithLifetime(WithLifetimeProps::builder().data(&val).build())
}
}

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

@@ -1,8 +1,9 @@
#[cfg(not(feature = "ssr"))]
pub mod tests {
use leptos::{
server,
server_fn::{codec, Http, ServerFn, ServerFnError},
server_fn::{codec, ServerFn, ServerFnError},
};
use std::any::TypeId;
@@ -18,8 +19,8 @@ pub mod tests {
"/api/my_server_action"
);
assert_eq!(
TypeId::of::<<MyServerAction as ServerFn>::Protocol>(),
TypeId::of::<Http<codec::PostUrl, codec::Json>>()
TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(),
TypeId::of::<codec::PostUrl>()
);
}
@@ -31,8 +32,8 @@ pub mod tests {
}
assert_eq!(<FooBar as ServerFn>::PATH, "/foo/bar/my_path");
assert_eq!(
TypeId::of::<<FooBar as ServerFn>::Protocol>(),
TypeId::of::<Http<codec::Cbor, codec::Cbor>>()
TypeId::of::<<FooBar as ServerFn>::InputEncoding>(),
TypeId::of::<codec::Cbor>()
);
}
@@ -44,8 +45,8 @@ pub mod tests {
}
assert_eq!(<FooBar as ServerFn>::PATH, "/foo/bar/my_path");
assert_eq!(
TypeId::of::<<FooBar as ServerFn>::Protocol>(),
TypeId::of::<Http<codec::Cbor, codec::Cbor>>()
TypeId::of::<<FooBar as ServerFn>::InputEncoding>(),
TypeId::of::<codec::Cbor>()
);
}
@@ -57,8 +58,8 @@ pub mod tests {
}
assert_eq!(<FooBar as ServerFn>::PATH, "/api/my_path");
assert_eq!(
TypeId::of::<<FooBar as ServerFn>::Protocol>(),
TypeId::of::<Http<codec::PostUrl, codec::Json>>()
TypeId::of::<<FooBar as ServerFn>::InputEncoding>(),
TypeId::of::<codec::PostUrl>()
);
}
@@ -73,8 +74,8 @@ pub mod tests {
"/api/my_server_action"
);
assert_eq!(
TypeId::of::<<FooBar as ServerFn>::Protocol>(),
TypeId::of::<Http<codec::PostUrl, codec::Json>>()
TypeId::of::<<FooBar as ServerFn>::InputEncoding>(),
TypeId::of::<codec::PostUrl>()
);
}
@@ -90,8 +91,8 @@ pub mod tests {
"/foo/bar/my_server_action"
);
assert_eq!(
TypeId::of::<<MyServerAction as ServerFn>::Protocol>(),
TypeId::of::<Http<codec::PostUrl, codec::Json>>()
TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(),
TypeId::of::<codec::PostUrl>()
);
}
@@ -107,8 +108,8 @@ pub mod tests {
"/api/my_server_action"
);
assert_eq!(
TypeId::of::<<MyServerAction as ServerFn>::Protocol>(),
TypeId::of::<Http<codec::GetUrl, codec::Json>>()
TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(),
TypeId::of::<codec::GetUrl>()
);
}
@@ -123,8 +124,8 @@ pub mod tests {
"/api/path/to/my/endpoint"
);
assert_eq!(
TypeId::of::<<MyServerAction as ServerFn>::Protocol>(),
TypeId::of::<Http<codec::PostUrl, codec::Json>>()
TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(),
TypeId::of::<codec::PostUrl>()
);
}
}

View File

@@ -1,4 +1,4 @@
#[cfg(not(feature = "__internal_erase_components"))]
#[cfg(not(erase_components))]
#[test]
fn ui() {
let t = trybuild::TestCases::new();

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

@@ -1,8 +1,6 @@
[package]
name = "leptos_server"
# TODO revert to { workspace = true } before 0.8.0 release
# this is a hack because I missing bumping the hydration_context version number before publishing
version = "0.8.0-alpha2"
version = { workspace = true }
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
@@ -13,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 }
@@ -27,9 +25,9 @@ send_wrapper = "0.6"
# serialization formats
serde = { version = "1.0" }
js-sys = { version = "0.3.74", optional = true }
wasm-bindgen = { version = "0.2.100", optional = true }
serde_json = { workspace = true }
js-sys = { version = "0.3.72", optional = true }
wasm-bindgen = { version = "0.2.95", optional = true }
serde_json = { version = "1.0" }
[features]
ssr = []
@@ -46,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,7 +3,7 @@ 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.
@@ -42,8 +42,8 @@ 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 +52,20 @@ 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 +76,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 +91,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,11 +114,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
}
@@ -132,8 +131,8 @@ 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>,
}
@@ -147,14 +146,14 @@ where
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 +182,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 +217,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

@@ -79,7 +79,7 @@ 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,
@@ -135,7 +135,6 @@ mod view_implementations {
Ser: Send + 'static,
{
type AsyncOutput = Option<T>;
type Owned = Self;
const MIN_LENGTH: usize = 0;
@@ -153,14 +152,12 @@ mod view_implementations {
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) {
(move || Suspend::new(async move { self.await })).to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs,
);
}
@@ -170,7 +167,6 @@ mod view_implementations {
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) where
Self: Sized,
{
@@ -180,7 +176,6 @@ mod view_implementations {
position,
escape,
mark_branches,
extra_attrs,
);
}
@@ -192,9 +187,5 @@ mod view_implementations {
(move || Suspend::new(async move { self.await }))
.hydrate::<FROM_SERVER>(cursor, position)
}
fn into_owned(self) -> Self::Owned {
self
}
}
}

View File

@@ -8,13 +8,8 @@ use reactive_graph::{
ToAnySource, ToAnySubscriber,
},
owner::use_context,
signal::{
guards::{AsyncPlain, ReadGuard},
ArcRwSignal, RwSignal,
},
traits::{
DefinedAt, IsDisposed, ReadUntracked, Track, Update, With, Write,
},
signal::guards::{AsyncPlain, ReadGuard},
traits::{DefinedAt, IsDisposed, ReadUntracked},
};
use send_wrapper::SendWrapper;
use std::{
@@ -25,8 +20,7 @@ use std::{
/// 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>,
}
@@ -34,8 +28,7 @@ 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,
}
}
@@ -72,55 +65,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;
}
/// 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(&SendWrapper<T>) -> U) -> Option<U>
where
T: 'static,
{
self.data.try_with(|n| n.as_ref().map(f))?
}
}
impl<T, E> ArcLocalResource<Result<T, E>>
where
T: 'static,
E: 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()))
}
}
impl<T> IntoFuture for ArcLocalResource<T>
@@ -151,11 +104,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
}
@@ -247,8 +200,7 @@ 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>,
}
@@ -290,7 +242,6 @@ impl<T> LocalResource<T> {
}
}
};
let refetch = RwSignal::new(0);
Self {
data: if cfg!(feature = "ssr") {
@@ -298,21 +249,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>
@@ -343,11 +287,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
}
@@ -454,8 +398,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,
}
}
@@ -465,8 +408,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,7 +2,7 @@ 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.
@@ -11,8 +11,8 @@ 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>,
}
@@ -29,7 +29,7 @@ where
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 +40,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 +55,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,11 +78,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
}
@@ -95,13 +95,13 @@ 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,
@@ -123,7 +123,7 @@ where
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 +152,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 +176,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
}

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