Compare commits

...

23 Commits

Author SHA1 Message Date
Greg Johnston
adaa08c9df chore: resolve new warnings 2025-12-13 08:59:02 -05:00
Greg Johnston
779b2f2a9f chore: bump versions after recent release 2025-11-24 19:49:03 -05:00
Greg Johnston
72e0abc75c chore: bump versions after recent release (#4462) 2025-11-24 17:29:23 -05:00
zakstucke
a7a8970150 feat: resupport From<Fn() -> T> for Signal<T>, ArcSignal<T>, Callback<T, _> and similar (#4273) 2025-11-24 13:50:11 -05:00
Tyler Earls
2e09f3d102 fix: make class attribute overwrite behavior consistent between SSR and CSR (closes #4248) (#4439)
Fixes #4248

During SSR, multiple `class` attributes were incorrectly concatenating
instead of overwriting like they do in browsers. This inconsistency
caused code that appeared to work in SSR to fail in CSR/hydration.

The fix distinguishes between two types of class attributes:
- `class="..."` attributes should overwrite (clear previous values)
- `class:name=value` directives should merge (append to existing classes)

Implementation:
- Added `should_overwrite()` method to `IntoClass` trait (defaults to `false`)
- Modified `Class::to_html()` to clear buffer before rendering if `should_overwrite()` returns `true`
- Implemented `should_overwrite() -> true` for string types (`&str`, `String`, `Cow<'_, str>`, `Arc<str>`)
- Tuple type `(&'static str, bool)` keeps default `false` for merge behavior

Added comprehensive tests to verify:
- `class="foo" class:bar=true` produces `"foo bar"` (merge)
- `class:foo=true` works standalone
- Correct behavior with macro attribute sorting
- Global class application

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-22 13:12:10 -05:00
tqq1994516
e6fe7fef07 fix: add response headers for leptos_axum static files #4377 (#4394) 2025-11-22 13:11:53 -05:00
Greg Johnston
629f4f9d0f fix: do not unescape query and hash in URLs when clicking links (closes (#4454) 2025-11-21 13:16:24 -05:00
Ægir Örn Símonarson
ff5b612e12 chore: removed duplicate workspace member oco (#4445) 2025-11-19 19:59:14 -05:00
Greg Johnston
61571ed24b fix: improve marker-node filtering when using islands router (closes #4443) (#4446) 2025-11-19 19:56:44 -05:00
Greg Johnston
4f3a26ce88 fix: track resources in Suspense that are read conditionally behind other resource reads (see #4430) (#4444) 2025-11-17 21:36:56 -05:00
Greg Johnston
83a848b5ec chore: clean up up warning behavior for resources that depend on other resources (#4415) (closes #3372) 2025-11-17 21:00:41 -05:00
Greg Johnston
eec9edf517 Update README.md 2025-11-11 15:51:21 -05:00
Marco Kuoni
861dcf354c docs: add --split to command for lazy_routes example (#4440) 2025-11-09 20:23:40 -05:00
Greg Johnston
af3d6cba22 fix: remove possibility of SendWrapper errors on server by using conditional compilation instead of overloading .dry_resolve() (closes #4432, #4402) (#4433) 2025-11-07 13:43:18 -05:00
Alexis Fontaine
a0d657f9b1 chore: relax Debug trait bound on tuples PossibleRouteMatch implementation (#4428) 2025-11-04 08:42:38 -05:00
Greg Johnston
cddb24ebd3 fix: check custom element tag name when rebuilding (#4413) 2025-11-02 14:49:51 -05:00
Greg Johnston
e8afd11995 Merge pull request #4427 from zakstucke/zak/root-owner-cleanup
Force cleanup even if there are other references to the root owner
2025-11-02 14:49:29 -05:00
Zak Stucke
4d01d95175 Clippy 2025-11-02 16:16:03 +02:00
Zak Stucke
9bf5b22633 Force cleanup even if there are other references to the root owner 2025-11-02 15:24:26 +02:00
Greg Johnston
da4a7d5285 examples: clarify behavior of upload-with-progress demo (closes #4397) (#4420) 2025-10-29 21:07:52 -04:00
Tim Sweña (Swast)
2af6c6353c chore: add homepage to leptos cargo metadata (#4417)
This updates the links at Are We Web Yet.
2025-10-29 08:35:13 -04:00
Greg Johnston
7f4b5eb4d1 chore: publish patch versions 2025-10-27 20:05:04 -04:00
WorldSEnder
fbf46ca58c feat: replace vendored wasm-split with out-of-repository version (#4369) 2025-10-24 21:13:55 -04:00
56 changed files with 1054 additions and 655 deletions

47
Cargo.lock generated
View File

@@ -1695,7 +1695,7 @@ dependencies = [
"libc",
"percent-encoding",
"pin-project-lite",
"socket2 0.5.10",
"socket2 0.6.0",
"tokio",
"tower-service",
"tracing",
@@ -1920,7 +1920,7 @@ checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
[[package]]
name = "leptos"
version = "0.8.11"
version = "0.8.14"
dependencies = [
"any_spawner",
"base64",
@@ -2004,7 +2004,7 @@ dependencies = [
[[package]]
name = "leptos_axum"
version = "0.8.6"
version = "0.8.7"
dependencies = [
"any_spawner",
"axum",
@@ -2027,7 +2027,7 @@ dependencies = [
[[package]]
name = "leptos_config"
version = "0.8.7"
version = "0.8.8"
dependencies = [
"config",
"regex",
@@ -2074,7 +2074,7 @@ dependencies = [
[[package]]
name = "leptos_integration_utils"
version = "0.8.6"
version = "0.8.7"
dependencies = [
"futures",
"hydration_context",
@@ -2087,7 +2087,7 @@ dependencies = [
[[package]]
name = "leptos_macro"
version = "0.8.10"
version = "0.8.12"
dependencies = [
"attribute-derive",
"cfg-if",
@@ -2131,7 +2131,7 @@ dependencies = [
[[package]]
name = "leptos_router"
version = "0.8.9"
version = "0.8.10"
dependencies = [
"any_spawner",
"either_of",
@@ -2155,7 +2155,7 @@ dependencies = [
[[package]]
name = "leptos_router_macro"
version = "0.8.5"
version = "0.8.6"
dependencies = [
"leptos",
"leptos_macro",
@@ -2168,7 +2168,7 @@ dependencies = [
[[package]]
name = "leptos_server"
version = "0.8.5"
version = "0.8.6"
dependencies = [
"any_spawner",
"base64",
@@ -2796,7 +2796,7 @@ dependencies = [
"quinn-udp",
"rustc-hash 2.1.1",
"rustls",
"socket2 0.5.10",
"socket2 0.6.0",
"thiserror 2.0.17",
"tokio",
"tracing",
@@ -2833,7 +2833,7 @@ dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2 0.5.10",
"socket2 0.6.0",
"tracing",
"windows-sys 0.60.2",
]
@@ -2915,7 +2915,7 @@ dependencies = [
[[package]]
name = "reactive_graph"
version = "0.2.9"
version = "0.2.11"
dependencies = [
"any_spawner",
"async-lock",
@@ -2924,6 +2924,7 @@ dependencies = [
"hydration_context",
"indexmap",
"or_poisoned",
"paste",
"pin-project-lite",
"rustc-hash 2.1.1",
"rustc_version",
@@ -2935,6 +2936,7 @@ dependencies = [
"tokio",
"tokio-test",
"tracing",
"typed-builder",
"web-sys",
]
@@ -3791,7 +3793,7 @@ dependencies = [
[[package]]
name = "tachys"
version = "0.2.10"
version = "0.2.11"
dependencies = [
"any_spawner",
"async-trait",
@@ -4307,18 +4309,18 @@ dependencies = [
[[package]]
name = "typed-builder"
version = "0.21.2"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fef81aec2ca29576f9f6ae8755108640d0a86dd3161b2e8bca6cfa554e98f77d"
checksum = "398a3a3c918c96de527dc11e6e846cd549d4508030b8a33e1da12789c856b81a"
dependencies = [
"typed-builder-macro",
]
[[package]]
name = "typed-builder-macro"
version = "0.21.2"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ecb9ecf7799210407c14a8cfdfe0173365780968dc57973ed082211958e0b18"
checksum = "0e48cea23f68d1f78eb7bc092881b6bb88d3d6b5b7e6234f6f9c911da1ffb221"
dependencies = [
"proc-macro2",
"quote",
@@ -4602,23 +4604,24 @@ dependencies = [
[[package]]
name = "wasm_split_helpers"
version = "0.1.2"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a114b3073258dd5de3d812cdd048cca6842342755e828a14dbf15f843f2d1b84"
dependencies = [
"async-once-cell",
"or_poisoned",
"wasm_split_macros",
]
[[package]]
name = "wasm_split_macros"
version = "0.1.3"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56481f8ed1a9f9ae97ea7b08a5e2b12e8adf9a7818a6ba952b918e09c7be8bf0"
dependencies = [
"base16",
"digest",
"quote",
"sha2",
"syn 2.0.106",
"wasm-bindgen",
]
[[package]]

View File

@@ -2,7 +2,6 @@
resolver = "2"
members = [
# utilities
"oco",
"any_spawner",
"const_str_slice_concat",
"either_of",
@@ -50,28 +49,26 @@ any_spawner = { path = "./any_spawner/", version = "0.3.0" }
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1" }
either_of = { path = "./either_of/", version = "0.1.6" }
hydration_context = { path = "./hydration_context", version = "0.3.0" }
leptos = { path = "./leptos", version = "0.8.11" }
leptos_config = { path = "./leptos_config", version = "0.8.7" }
leptos = { path = "./leptos", version = "0.8.14" }
leptos_config = { path = "./leptos_config", version = "0.8.8" }
leptos_dom = { path = "./leptos_dom", version = "0.8.7" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.5" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.8.6" }
leptos_macro = { path = "./leptos_macro", version = "0.8.10" }
leptos_router = { path = "./router", version = "0.8.9" }
leptos_router_macro = { path = "./router_macro", version = "0.8.5" }
leptos_server = { path = "./leptos_server", version = "0.8.5" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.8.7" }
leptos_macro = { path = "./leptos_macro", version = "0.8.12" }
leptos_router = { path = "./router", version = "0.8.10" }
leptos_router_macro = { path = "./router_macro", version = "0.8.6" }
leptos_server = { path = "./leptos_server", version = "0.8.6" }
leptos_meta = { path = "./meta", version = "0.8.5" }
next_tuple = { path = "./next_tuple", version = "0.1.0" }
oco_ref = { path = "./oco", version = "0.2.1" }
or_poisoned = { path = "./or_poisoned", version = "0.1.0" }
reactive_graph = { path = "./reactive_graph", version = "0.2.9" }
reactive_graph = { path = "./reactive_graph", version = "0.2.11" }
reactive_stores = { path = "./reactive_stores", version = "0.3.0" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.2.6" }
server_fn = { path = "./server_fn", version = "0.8.8" }
server_fn_macro = { path = "./server_fn_macro", version = "0.8.8" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.5" }
tachys = { path = "./tachys", version = "0.2.10" }
wasm_split_helpers = { path = "./wasm_split", version = "0.1.2" }
wasm_split_macros = { path = "./wasm_split_macros", version = "0.1.3" }
tachys = { path = "./tachys", version = "0.2.11" }
# members deps
async-once-cell = { default-features = false, version = "0.5.3" }
@@ -79,7 +76,8 @@ itertools = { default-features = false, version = "0.14.0" }
convert_case = { default-features = false, version = "0.8.0" }
serde_json = { default-features = false, version = "1.0.143" }
trybuild = { default-features = false, version = "1.0.110" }
typed-builder = { default-features = false, version = "0.21.2" }
typed-builder = { default-features = false, version = "0.22.0" }
typed-builder-macro = { default-features = false, version = "0.22.0" }
thiserror = { default-features = false, version = "2.0.17" }
wasm-bindgen = { default-features = false, version = "0.2.100" }
indexmap = { default-features = false, version = "2.11.0" }
@@ -123,7 +121,6 @@ serial_test = { default-features = false, version = "3.2.0" }
erased = { default-features = false, version = "0.1.2" }
glib = { default-features = false, version = "0.20.12" }
async-trait = { default-features = false, version = "0.1.89" }
typed-builder-macro = { default-features = false, version = "0.21.0" }
linear-map = { default-features = false, version = "1.2.0" }
anyhow = { default-features = false, version = "1.0.100" }
walkdir = { default-features = false, version = "2.5.0" }
@@ -174,6 +171,7 @@ sha2 = { default-features = false, version = "0.10.8" }
subsecond = { default-features = false, version = "0.7.0-rc.0" }
dioxus-cli-config = { default-features = false, version = "0.7.0-rc.0" }
dioxus-devtools = { default-features = false, version = "0.7.0-rc.0" }
wasm_split_helpers = { default-features = false, version = "0.2.0" }
[profile.release]
codegen-units = 1

View File

@@ -9,6 +9,3 @@ routing when you use islands.
This uses *only* server rendering, with no actual islands, but still maintains client-side state across page navigations.
It does this by building on the fact that we now have a statically-typed view tree to do pretty smart updates with
new HTML from the client, with extremely minimal diffing.
The demo itself works, but the feature that supports it is incomplete. A couple people have accidentally
used it and broken their applications in ways they don't understand, so I've renamed the feature to `dont-use-islands-router`.

View File

@@ -5,4 +5,4 @@ test cases that typically happens at integration.
## Quick Start
Run `cargo leptos watch` to run this example.
Run `cargo leptos watch --split` to run this example.

View File

@@ -440,7 +440,14 @@ pub fn FileUploadWithProgress() -> impl IntoView {
let mut entry =
FILES.entry(filename.to_string()).or_insert_with(|| {
println!("[{filename}]\tinserting channel");
let (tx, rx) = broadcast(128);
// NOTE: this channel capacity is set arbitrarily for this demo code.
// it allows for up to exactly 1048 chunks to be sent, which sets an upper cap
// on upload size (the precise details vary by client)
// in a real system, you will want to create some more reasonable ways of
// sending and sharing notifications
//
// see https://github.com/leptos-rs/leptos/issues/4397 for related discussion
let (tx, rx) = broadcast(1048);
File { total: 0, tx, rx }
});
entry.total += len;

View File

@@ -1,7 +1,10 @@
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use axum::Router;
use axum::{
http::{HeaderName, HeaderValue},
Router,
};
use leptos::{logging::log, prelude::*};
use leptos_axum::{generate_route_list, LeptosRoutes};
use ssr_modes_axum::app::*;
@@ -17,7 +20,24 @@ async fn main() {
let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone())
})
.fallback(leptos_axum::file_and_error_handler(shell))
.fallback(leptos_axum::file_and_error_handler_with_context(
move || {
// if you want to add custom headers to the static file handler response,
// you can do that by providing `ResponseOptions` via context
let opts = use_context::<leptos_axum::ResponseOptions>()
.unwrap_or_default();
opts.insert_header(
HeaderName::from_static("cross-origin-opener-policy"),
HeaderValue::from_static("same-origin"),
);
opts.insert_header(
HeaderName::from_static("cross-origin-embedder-policy"),
HeaderValue::from_static("require-corp"),
);
provide_context(opts);
},
shell,
))
.with_state(leptos_options);
// run our app with hyper

View File

@@ -4,7 +4,7 @@ authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
description = "Axum integrations for the Leptos web framework."
version = "0.8.6"
version = "0.8.7"
rust-version.workspace = true
edition.workspace = true

View File

@@ -2050,7 +2050,20 @@ where
let res = res.await.unwrap();
if res.status() == StatusCode::OK {
res.into_response()
let owner = Owner::new();
owner.with(|| {
additional_context();
let res = res.into_response();
if let Some(response_options) =
use_context::<ResponseOptions>()
{
let mut res = AxumResponse(res);
res.extend_response(&response_options);
res.0
} else {
res
}
})
} else {
let mut res = handle_response_inner(
move || {

View File

@@ -4,7 +4,7 @@ authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
description = "Utilities to help build server integrations for the Leptos web framework."
version = "0.8.6"
version = "0.8.7"
rust-version.workspace = true
edition.workspace = true

View File

@@ -121,7 +121,7 @@ pub trait ExtendResponse: Sized {
// drop the owner, cleaning up the reactive runtime,
// once the stream is over
.chain(once(async move {
owner.unset();
owner.unset_with_forced_cleanup();
Default::default()
})),
));

View File

@@ -1,9 +1,10 @@
[package]
name = "leptos"
version = "0.8.11"
version = "0.8.14"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
homepage = "https://leptos.dev/"
description = "Leptos is a full-stack, isomorphic Rust web framework leveraging fine-grained reactivity to build declarative user interfaces."
readme = "../README.md"
rust-version.workspace = true
@@ -57,7 +58,7 @@ serde_qs = { workspace = true, default-features = true }
slotmap = { workspace = true, default-features = true }
futures = { workspace = true, default-features = true }
send_wrapper = { workspace = true, default-features = true }
wasm_split_helpers.workspace = true
wasm_split_helpers = { workspace = true, default-features = true }
subsecond = { workspace = true, default-features = true, optional = true }
dioxus-cli-config = { workspace = true, default-features = true, optional = true }
dioxus-devtools = { workspace = true, default-features = true, optional = true }

View File

@@ -203,7 +203,7 @@ pub mod prelude {
pub mod form;
/// A standard way to wrap functions and closures to pass them to components.
pub mod callback;
pub use reactive_graph::callback;
/// Types that can be passed as the `children` prop of a component.
pub mod children;
@@ -365,7 +365,8 @@ pub use serde_json;
pub use tracing;
#[doc(hidden)]
pub use wasm_bindgen;
pub use wasm_split_helpers;
#[doc(hidden)]
pub use wasm_split_helpers as wasm_split;
#[doc(hidden)]
pub use web_sys;

View File

@@ -6,6 +6,7 @@ use crate::{
use futures::{channel::oneshot, select, FutureExt};
use hydration_context::SerializedDataId;
use leptos_macro::component;
use or_poisoned::OrPoisoned;
use reactive_graph::{
computed::{
suspense::{LocalResourceNotifier, SuspenseContext},
@@ -14,10 +15,10 @@ use reactive_graph::{
effect::RenderEffect,
owner::{provide_context, use_context, Owner},
signal::ArcRwSignal,
traits::{Dispose, Get, Read, Track, With, WriteValue},
traits::{Dispose, Get, Read, ReadUntracked, Track, With, WriteValue},
};
use slotmap::{DefaultKey, SlotMap};
use std::sync::Arc;
use std::sync::{Arc, Mutex};
use tachys::{
either::Either,
html::attribute::{any_attribute::AnyAttribute, Attribute},
@@ -320,23 +321,66 @@ where
// walk over the tree of children once to make sure that all resource loads are registered
self.children.dry_resolve();
let children = Arc::new(Mutex::new(Some(self.children)));
// check the set of tasks to see if it is empty, now or later
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 let Some(tx) = notify_error_boundary.take() {
_ = tx.send(());
let children = Arc::clone(&children);
move |double_checking: Option<bool>| {
// on the first run, always track the tasks
if double_checking.is_none() {
tasks.track();
}
if let Some(curr_tasks) = tasks.try_read_untracked() {
if curr_tasks.is_empty() {
if double_checking == Some(true) {
// we have finished loading, and checking the children again told us there are
// no more pending tasks. so we can render both the children and the error boundary
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 let Some(tx) = notify_error_boundary.take() {
_ = tx.send(());
}
} else {
// release the read guard on tasks, as we'll be updating it again
drop(curr_tasks);
// check the children for additional pending tasks
// the will catch additional resource reads nested inside a conditional depending on initial resource reads
if let Some(children) =
children.lock().or_poisoned().as_mut()
{
children.dry_resolve();
}
if tasks
.try_read()
.map(|n| n.is_empty())
.unwrap_or(false)
{
// there are no additional pending tasks, and we can simply return
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 let Some(tx) = notify_error_boundary.take() {
_ = tx.send(());
}
}
// tell ourselves that we're just double-checking
return true;
}
} else {
tasks.track();
}
}
false
}
});
@@ -362,12 +406,17 @@ where
None
}
_ = tasks_rx => {
let children = {
let mut children_lock = children.lock().or_poisoned();
children_lock.take().expect("children should not be removed until we render here")
};
// if we ran this earlier, reactive reads would always be registered as None
// this is fine in the case where we want to use Suspend and .await on some future
// but in situations like a <For each=|| some_resource.snapshot()/> we actually
// want to be able to 1) synchronously read a resource's value, but still 2) wait
// for it to load before we render everything
let mut children = Box::pin(self.children.resolve().fuse());
let mut children = Box::pin(children.resolve().fuse());
// we continue racing the children against the "do we have any local
// resources?" Future

View File

@@ -103,6 +103,76 @@ fn test_classes() {
assert_eq!(rendered.to_html(), "<div class=\"my big red car\"></div>");
}
#[cfg(feature = "ssr")]
#[test]
fn test_class_with_class_directive_merge() {
use leptos::prelude::*;
// class= followed by class: should merge
let rendered: View<HtmlElement<_, _, _>> = view! {
<div class="foo" class:bar=true></div>
};
assert_eq!(rendered.to_html(), "<div class=\"foo bar\"></div>");
}
#[cfg(feature = "ssr")]
#[test]
fn test_solo_class_directive() {
use leptos::prelude::*;
// Solo class: directive should work without class attribute
let rendered: View<HtmlElement<_, _, _>> = view! {
<div class:foo=true></div>
};
assert_eq!(rendered.to_html(), "<div class=\"foo\"></div>");
}
#[cfg(feature = "ssr")]
#[test]
fn test_class_directive_with_static_class() {
use leptos::prelude::*;
// class:foo comes after class= due to macro sorting
// The class= clears buffer, then class:foo appends
let rendered: View<HtmlElement<_, _, _>> = view! {
<div class:foo=true class="bar"></div>
};
// After macro sorting: class="bar" class:foo=true
// Expected: "bar foo"
assert_eq!(rendered.to_html(), "<div class=\"bar foo\"></div>");
}
#[cfg(feature = "ssr")]
#[test]
fn test_global_class_applied() {
use leptos::prelude::*;
// Test that a global class is properly applied
let rendered: View<HtmlElement<_, _, _>> = view! { class="global",
<div></div>
};
assert_eq!(rendered.to_html(), "<div class=\"global\"></div>");
}
#[cfg(feature = "ssr")]
#[test]
fn test_multiple_class_attributes_overwrite() {
use leptos::prelude::*;
// When multiple class attributes are applied, the last one should win (browser behavior)
// This simulates what happens when attributes are combined programmatically
let el = leptos::html::div().class("first").class("second");
let html = el.to_html();
// The second class attribute should overwrite the first
assert_eq!(html, "<div class=\"second\"></div>");
}
#[cfg(feature = "ssr")]
#[test]
fn ssr_with_styles() {

View File

@@ -5,7 +5,7 @@ license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
description = "Configuration for the Leptos web framework."
readme = "../README.md"
version = "0.8.7"
version = "0.8.8"
rust-version.workspace = true
edition.workspace = true

View File

@@ -221,18 +221,15 @@ fn env_w_default(
/// An enum that can be used to define the environment Leptos is running in.
/// Setting this to the `PROD` variant will not include the WebSocket code for `cargo-leptos` watch mode.
/// Defaults to `DEV`.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
#[derive(
Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, Default,
)]
pub enum Env {
PROD,
#[default]
DEV,
}
impl Default for Env {
fn default() -> Self {
Self::DEV
}
}
fn env_from_str(input: &str) -> Result<Env, LeptosConfigError> {
let sanitized = input.to_lowercase();
match sanitized.as_ref() {
@@ -279,18 +276,15 @@ impl TryFrom<String> for Env {
/// An enum that can be used to define the websocket protocol Leptos uses for hotreloading
/// Defaults to `ws`.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
#[derive(
Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, Default,
)]
pub enum ReloadWSProtocol {
#[default]
WS,
WSS,
}
impl Default for ReloadWSProtocol {
fn default() -> Self {
Self::WS
}
}
fn ws_from_str(input: &str) -> Result<ReloadWSProtocol, LeptosConfigError> {
let sanitized = input.to_lowercase();
match sanitized.as_ref() {

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_macro"
version = "0.8.10"
version = "0.8.12"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"

View File

@@ -1016,25 +1016,27 @@ struct PropOpt {
name: Option<String>,
}
struct TypedBuilderOpts {
struct TypedBuilderOpts<'a> {
default: bool,
default_with_value: Option<syn::Expr>,
strip_option: bool,
into: bool,
ty: &'a Type,
}
impl TypedBuilderOpts {
fn from_opts(opts: &PropOpt, is_ty_option: bool) -> Self {
impl<'a> TypedBuilderOpts<'a> {
fn from_opts(opts: &PropOpt, ty: &'a Type) -> Self {
Self {
default: opts.optional || opts.optional_no_strip || opts.attrs,
default_with_value: opts.default.clone(),
strip_option: opts.strip_option || opts.optional && is_ty_option,
strip_option: opts.strip_option || opts.optional && is_option(ty),
into: opts.into,
ty,
}
}
}
impl TypedBuilderOpts {
impl TypedBuilderOpts<'_> {
fn to_serde_tokens(&self) -> TokenStream {
let default = if let Some(v) = &self.default_with_value {
let v = v.to_token_stream().to_string();
@@ -1053,7 +1055,7 @@ impl TypedBuilderOpts {
}
}
impl ToTokens for TypedBuilderOpts {
impl ToTokens for TypedBuilderOpts<'_> {
fn to_tokens(&self, tokens: &mut TokenStream) {
let default = if let Some(v) = &self.default_with_value {
let v = v.to_token_stream().to_string();
@@ -1064,14 +1066,29 @@ impl ToTokens for TypedBuilderOpts {
quote! {}
};
let strip_option = if self.strip_option {
// If self.strip_option && self.into, then the strip_option will be represented as part of the transform closure.
let strip_option = if self.strip_option && !self.into {
quote! { strip_option, }
} else {
quote! {}
};
let into = if self.into {
quote! { into, }
if !self.strip_option {
let ty = &self.ty;
quote! {
fn transform<__IntoReactiveValueMarker>(value: impl ::leptos::prelude::IntoReactiveValue<#ty, __IntoReactiveValueMarker>) -> #ty {
value.into_reactive_value()
},
}
} else {
let ty = unwrap_option(self.ty);
quote! {
fn transform<__IntoReactiveValueMarker>(value: impl ::leptos::prelude::IntoReactiveValue<#ty, __IntoReactiveValueMarker>) -> Option<#ty> {
Some(value.into_reactive_value())
},
}
}
} else {
quote! {}
};
@@ -1107,8 +1124,7 @@ fn prop_builder_fields(
ty,
} = prop;
let builder_attrs =
TypedBuilderOpts::from_opts(prop_opts, is_option(ty));
let builder_attrs = TypedBuilderOpts::from_opts(prop_opts, ty);
let builder_docs = prop_to_doc(prop, PropDocStyle::Inline);
@@ -1153,8 +1169,7 @@ fn prop_serializer_fields(vis: &Visibility, props: &[Prop]) -> TokenStream {
ty,
} = prop;
let builder_attrs =
TypedBuilderOpts::from_opts(prop_opts, is_option(ty));
let builder_attrs = TypedBuilderOpts::from_opts(prop_opts, ty);
let serde_attrs = builder_attrs.to_serde_tokens();
let PatIdent { ident, by_ref, .. } = &name;

View File

@@ -2,12 +2,12 @@ use convert_case::{Case, Casing};
use proc_macro::TokenStream;
use proc_macro2::Ident;
use proc_macro_error2::abort;
use quote::quote;
use quote::{format_ident, quote};
use std::{
hash::{DefaultHasher, Hash, Hasher},
mem,
};
use syn::{parse_macro_input, ItemFn};
use syn::{parse_macro_input, parse_quote, ItemFn, ReturnType, Stmt};
pub fn lazy_impl(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
let name = if !args.is_empty() {
@@ -16,7 +16,7 @@ pub fn lazy_impl(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
None
};
let mut fun = syn::parse::<ItemFn>(s).unwrap_or_else(|e| {
let fun = syn::parse::<ItemFn>(s).unwrap_or_else(|e| {
abort!(e.span(), "`lazy` can only be used on a function")
});
@@ -47,29 +47,50 @@ pub fn lazy_impl(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
let is_wasm = cfg!(feature = "csr") || cfg!(feature = "hydrate");
if is_wasm {
let mut fun = fun;
let mut return_wrapper = None;
if was_async {
fun.sig.asyncness = None;
let ty = match &fun.sig.output {
ReturnType::Default => quote! { () },
ReturnType::Type(_, ty) => quote! { #ty },
};
let sync_output: ReturnType = parse_quote! {
-> ::std::pin::Pin<::std::boxed::Box<dyn ::std::future::Future<Output = #ty> + ::std::marker::Send + ::std::marker::Sync>>
};
let async_output = mem::replace(&mut fun.sig.output, sync_output);
let stmts = mem::take(&mut fun.block.stmts);
fun.block.stmts.push(Stmt::Expr(parse_quote! {
::std::boxed::Box::pin(::leptos::__reexports::send_wrapper::SendWrapper::new(async move {
#( #stmts )*
}))
}, None));
return_wrapper = Some(quote! {
return_wrapper(let future = _; { future.await } #async_output),
});
}
let preload_name = format_ident!("__preload_{}", fun.sig.ident);
quote! {
#[::leptos::wasm_split_helpers::wasm_split(
#[::leptos::wasm_split::wasm_split(
#unique_name,
::leptos::__reexports::send_wrapper
wasm_split_path = ::leptos::wasm_split,
preload(#[doc(hidden)] #[allow(non_snake_case)] #preload_name),
#return_wrapper
)]
#fun
}
} else {
let mut fun = fun;
if !was_async {
fun.sig.asyncness = Some(Default::default());
}
let statements = &mut fun.block.stmts;
let old_statements = mem::take(statements);
statements.push(
syn::parse(
quote! {
::leptos::prefetch_lazy_fn_on_server(#unique_name_str);
}
.into(),
)
.unwrap(),
);
statements.push(parse_quote! {
::leptos::prefetch_lazy_fn_on_server(#unique_name_str);
});
statements.extend(old_statements);
quote! { #fun }
}

View File

@@ -159,25 +159,27 @@ struct PropOpt {
pub attrs: bool,
}
struct TypedBuilderOpts {
struct TypedBuilderOpts<'a> {
default: bool,
default_with_value: Option<syn::Expr>,
strip_option: bool,
into: bool,
ty: &'a Type,
}
impl TypedBuilderOpts {
pub fn from_opts(opts: &PropOpt, is_ty_option: bool) -> Self {
impl<'a> TypedBuilderOpts<'a> {
pub fn from_opts(opts: &PropOpt, ty: &'a Type) -> Self {
Self {
default: opts.optional || opts.optional_no_strip || opts.attrs,
default_with_value: opts.default.clone(),
strip_option: opts.strip_option || opts.optional && is_ty_option,
strip_option: opts.strip_option || opts.optional && is_option(ty),
into: opts.into,
ty,
}
}
}
impl ToTokens for TypedBuilderOpts {
impl ToTokens for TypedBuilderOpts<'_> {
fn to_tokens(&self, tokens: &mut TokenStream) {
let default = if let Some(v) = &self.default_with_value {
let v = v.to_token_stream().to_string();
@@ -188,14 +190,29 @@ impl ToTokens for TypedBuilderOpts {
quote! {}
};
let strip_option = if self.strip_option {
// If self.strip_option && self.into, then the strip_option will be represented as part of the transform closure.
let strip_option = if self.strip_option && !self.into {
quote! { strip_option, }
} else {
quote! {}
};
let into = if self.into {
quote! { into, }
if !self.strip_option {
let ty = &self.ty;
quote! {
fn transform<__IntoReactiveValueMarker>(value: impl ::leptos::prelude::IntoReactiveValue<#ty, __IntoReactiveValueMarker>) -> #ty {
value.into_reactive_value()
},
}
} else {
let ty = unwrap_option(self.ty);
quote! {
fn transform<__IntoReactiveValueMarker>(value: impl ::leptos::prelude::IntoReactiveValue<#ty, __IntoReactiveValueMarker>) -> Option<#ty> {
Some(value.into_reactive_value())
},
}
}
} else {
quote! {}
};
@@ -227,8 +244,7 @@ fn prop_builder_fields(vis: &Visibility, props: &[Prop]) -> TokenStream {
ty,
} = prop;
let builder_attrs =
TypedBuilderOpts::from_opts(prop_opts, is_option(ty));
let builder_attrs = TypedBuilderOpts::from_opts(prop_opts, ty);
let builder_docs = prop_to_doc(prop, PropDocStyle::Inline);

View File

@@ -90,7 +90,7 @@ pub(crate) fn component_to_tokens(
if optional {
optional_props.push(quote! {
props.#name = { #value }.map(Into::into);
props.#name = { #value }.map(::leptos::prelude::IntoReactiveValue::into_reactive_value);
})
} else {
required_props.push(quote! {

View File

@@ -120,6 +120,124 @@ fn returns_static_lifetime() {
}
}
#[cfg(not(feature = "nightly"))]
#[component]
pub fn IntoReactiveValueTestComponentSignal(
#[prop(into)] arg1: Signal<String>,
#[prop(into)] arg2: Signal<String>,
#[prop(into)] arg3: Signal<String>,
#[prop(into)] arg4: Signal<usize>,
#[prop(into)] arg5: Signal<usize>,
#[prop(into)] arg6: Signal<usize>,
#[prop(into)] arg7: Signal<Option<usize>>,
#[prop(into)] arg8: ArcSignal<String>,
#[prop(into)] arg9: ArcSignal<String>,
#[prop(into)] arg10: ArcSignal<String>,
#[prop(into)] arg11: ArcSignal<usize>,
#[prop(into)] arg12: ArcSignal<usize>,
#[prop(into)] arg13: ArcSignal<usize>,
#[prop(into)] arg14: ArcSignal<Option<usize>>,
// Optionals:
#[prop(into, optional)] arg15: Option<Signal<usize>>,
#[prop(into, optional)] arg16_purposely_omitted: Option<Signal<usize>>,
#[prop(into, optional)] arg17: Option<Signal<usize>>,
#[prop(into, strip_option)] arg18: Option<Signal<usize>>,
) -> impl IntoView {
move || {
view! {
<div>
<p>{arg1.get()}</p>
<p>{arg2.get()}</p>
<p>{arg3.get()}</p>
<p>{arg4.get()}</p>
<p>{arg5.get()}</p>
<p>{arg6.get()}</p>
<p>{arg7.get()}</p>
<p>{arg8.get()}</p>
<p>{arg9.get()}</p>
<p>{arg10.get()}</p>
<p>{arg11.get()}</p>
<p>{arg12.get()}</p>
<p>{arg13.get()}</p>
<p>{arg14.get()}</p>
<p>{arg15.get()}</p>
<p>{arg16_purposely_omitted.get()}</p>
<p>{arg17.get()}</p>
<p>{arg18.get()}</p>
</div>
}
}
}
#[component]
pub fn IntoReactiveValueTestComponentCallback(
#[prop(into)] arg1: Callback<(), String>,
#[prop(into)] arg2: Callback<usize, String>,
#[prop(into)] arg3: Callback<(usize,), String>,
#[prop(into)] arg4: Callback<(usize, String), String>,
#[prop(into)] arg5: UnsyncCallback<(), String>,
#[prop(into)] arg6: UnsyncCallback<usize, String>,
#[prop(into)] arg7: UnsyncCallback<(usize,), String>,
#[prop(into)] arg8: UnsyncCallback<(usize, String), String>,
) -> impl IntoView {
move || {
view! {
<div>
<p>{arg1.run(())}</p>
<p>{arg2.run(1)}</p>
<p>{arg3.run((2,))}</p>
<p>{arg4.run((3, "three".into()))}</p>
<p>{arg5.run(())}</p>
<p>{arg6.run(1)}</p>
<p>{arg7.run((2,))}</p>
<p>{arg8.run((3, "three".into()))}</p>
</div>
}
}
}
#[cfg(not(feature = "nightly"))]
#[test]
fn test_into_reactive_value_signal() {
let _ = view! {
<IntoReactiveValueTestComponentSignal
arg1=move || "I was a reactive closure!"
arg2="I was a basic str!"
arg3=Signal::stored("I was already a signal!")
arg4=move || 2
arg5=3
arg6=Signal::stored(4)
arg7=|| 2
arg8=move || "I was a reactive closure!"
arg9="I was a basic str!"
arg10=ArcSignal::stored("I was already a signal!".to_string())
arg11=move || 2
arg12=3
arg13=ArcSignal::stored(4)
arg14=|| 2
arg15=|| 2
nostrip:arg17=Some(|| 2)
arg18=|| 2
/>
};
}
#[test]
fn test_into_reactive_value_callback() {
let _ = view! {
<IntoReactiveValueTestComponentCallback
arg1=|| "I was a callback static str!"
arg2=|_n| "I was a callback static str!"
arg3=|(_n,)| "I was a callback static str!"
arg4=|(_n, _s)| "I was a callback static str!"
arg5=|| "I was a callback static str!"
arg6=|_n| "I was a callback static str!"
arg7=|(_n,)| "I was a callback static str!"
arg8=|(_n, _s)| "I was a callback static str!"
/>
};
}
// an attempt to catch unhygienic macros regression
mod macro_hygiene {
// To ensure no relative module path to leptos inside macros.
@@ -152,12 +270,7 @@ mod macro_hygiene {
#[component]
fn Component() -> impl IntoView {
view! {
<div>
{().into_any()}
{()}
</div>
}
view! { <div>{().into_any()} {()}</div> }
}
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_server"
version = "0.8.5"
version = "0.8.6"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"

View File

@@ -188,6 +188,39 @@ where
}
}
#[cfg(debug_assertions)]
thread_local! {
static RESOURCE_SOURCE_SIGNAL_ACTIVE: AtomicBool = const { AtomicBool::new(false) };
}
#[cfg(debug_assertions)]
/// Returns whether the current thread is currently running a resource source signal.
pub fn in_resource_source_signal() -> bool {
RESOURCE_SOURCE_SIGNAL_ACTIVE
.with(|scope| scope.load(std::sync::atomic::Ordering::Relaxed))
}
/// Set a static to true whilst running the given function.
/// [`is_in_effect_scope`] will return true whilst the function is running.
fn run_in_resource_source_signal<T>(fun: impl FnOnce() -> T) -> T {
#[cfg(debug_assertions)]
{
// For the theoretical nested case, set back to initial value rather than false:
let initial = RESOURCE_SOURCE_SIGNAL_ACTIVE.with(|scope| {
scope.swap(true, std::sync::atomic::Ordering::Relaxed)
});
let result = fun();
RESOURCE_SOURCE_SIGNAL_ACTIVE.with(|scope| {
scope.store(initial, std::sync::atomic::Ordering::Relaxed)
});
result
}
#[cfg(not(debug_assertions))]
{
fun()
}
}
impl<T, Ser> ReadUntracked for ArcResource<T, Ser>
where
T: 'static,
@@ -202,7 +235,9 @@ where
computed::suspense::SuspenseContext, effect::in_effect_scope,
owner::use_context,
};
if !in_effect_scope() && use_context::<SuspenseContext>().is_none()
if !in_effect_scope()
&& !in_resource_source_signal()
&& use_context::<SuspenseContext>().is_none()
{
let location = std::panic::Location::caller();
reactive_graph::log_warning(format_args!(
@@ -271,7 +306,7 @@ where
let refetch = ArcRwSignal::new(0);
let source = ArcMemo::new({
let refetch = refetch.clone();
move |_| (refetch.get(), source())
move |_| (refetch.get(), run_in_resource_source_signal(&source))
});
let fun = {
let source = source.clone();
@@ -909,7 +944,9 @@ where
computed::suspense::SuspenseContext, effect::in_effect_scope,
owner::use_context,
};
if !in_effect_scope() && use_context::<SuspenseContext>().is_none()
if !in_effect_scope()
&& !in_resource_source_signal()
&& use_context::<SuspenseContext>().is_none()
{
let location = std::panic::Location::caller();
reactive_graph::log_warning(format_args!(

View File

@@ -1,6 +1,6 @@
[package]
name = "reactive_graph"
version = "0.2.9"
version = "0.2.11"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
@@ -29,6 +29,7 @@ send_wrapper = { features = [
], workspace = true, default-features = true }
subsecond = { workspace = true, default-features = true, optional = true }
indexmap = { workspace = true, default-features = true }
paste = { workspace = true, default-features = true }
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
web-sys = { version = "0.3.77", features = ["console"] }
@@ -40,6 +41,7 @@ tokio = { features = [
], workspace = true, default-features = true }
tokio-test = { workspace = true, default-features = true }
any_spawner = { workspace = true, features = ["futures-executor", "tokio"] }
typed-builder.workspace = true
[build-dependencies]
rustc_version = { workspace = true, default-features = true }

View File

@@ -2,48 +2,19 @@
//! for component properties, because they can be used to define optional callback functions,
//! which generic props dont support.
//!
//! # Usage
//! Callbacks can be created manually from any function or closure, but the easiest way
//! to create them is to use `#[prop(into)]]` when defining a component.
//! ```
//! use leptos::prelude::*;
//!
//! #[component]
//! fn MyComponent(
//! #[prop(into)] render_number: Callback<(i32,), String>,
//! ) -> impl IntoView {
//! view! {
//! <div>
//! {render_number.run((1,))}
//! // callbacks can be called multiple times
//! {render_number.run((42,))}
//! </div>
//! }
//! }
//! // you can pass a closure directly as `render_number`
//! fn test() -> impl IntoView {
//! view! {
//! <MyComponent render_number=|x: i32| x.to_string()/>
//! }
//! }
//! ```
//!
//! *Notes*:
//! - The `render_number` prop can receive any type that implements `Fn(i32) -> String`.
//! - Callbacks are most useful when you want optional generic props.
//! - All callbacks implement the [`Callable`](leptos::callback::Callable) trait, and can be invoked with `my_callback.run(input)`.
//! - The callback types implement [`Copy`], so they can easily be moved into and out of other closures, just like signals.
//! The callback types implement [`Copy`], so they can easily be moved into and out of other closures, just like signals.
//!
//! # Types
//! This modules implements 2 callback types:
//! - [`Callback`](leptos::callback::Callback)
//! - [`UnsyncCallback`](leptos::callback::UnsyncCallback)
//! - [`Callback`](reactive_graph::callback::Callback)
//! - [`UnsyncCallback`](reactive_graph::callback::UnsyncCallback)
//!
//! Use `SyncCallback` if the function is not `Sync` and `Send`.
use reactive_graph::{
use crate::{
owner::{LocalStorage, StoredValue},
traits::{Dispose, WithValue},
IntoReactiveValue,
};
use std::{fmt, rc::Rc, sync::Arc};
@@ -60,7 +31,16 @@ pub trait Callable<In: 'static, Out: 'static = ()> {
fn run(&self, input: In) -> Out;
}
/// A callback type that is not required to be `Send + Sync`.
/// A callback type that is not required to be [`Send`] or [`Sync`].
///
/// # Example
/// ```
/// # use reactive_graph::prelude::*; use reactive_graph::callback::*; let owner = reactive_graph::owner::Owner::new(); owner.set();
/// let _: UnsyncCallback<()> = UnsyncCallback::new(|_| {});
/// let _: UnsyncCallback<(i32, i32)> = (|_x: i32, _y: i32| {}).into();
/// let cb: UnsyncCallback<i32, String> = UnsyncCallback::new(|x: i32| x.to_string());
/// assert_eq!(cb.run(42), "42".to_string());
/// ```
pub struct UnsyncCallback<In: 'static, Out: 'static = ()>(
StoredValue<Rc<dyn Fn(In) -> Out>, LocalStorage>,
);
@@ -148,28 +128,15 @@ impl_unsync_callable_from_fn!(
P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12
);
/// Callbacks define a standard way to store functions and closures.
/// A callback type that is [`Send`] + [`Sync`].
///
/// # Example
/// ```
/// # use leptos::prelude::*;
/// # use leptos::callback::{Callable, Callback};
/// #[component]
/// fn MyComponent(
/// #[prop(into)] render_number: Callback<(i32,), String>,
/// ) -> impl IntoView {
/// view! {
/// <div>
/// {render_number.run((42,))}
/// </div>
/// }
/// }
///
/// fn test() -> impl IntoView {
/// view! {
/// <MyComponent render_number=move |x: i32| x.to_string()/>
/// }
/// }
/// # use reactive_graph::prelude::*; use reactive_graph::callback::*; let owner = reactive_graph::owner::Owner::new(); owner.set();
/// let _: Callback<()> = Callback::new(|_| {});
/// let _: Callback<(i32, i32)> = (|_x: i32, _y: i32| {}).into();
/// let cb: Callback<i32, String> = Callback::new(|x: i32| x.to_string());
/// assert_eq!(cb.run(42), "42".to_string());
/// ```
pub struct Callback<In, Out = ()>(
StoredValue<Arc<dyn Fn(In) -> Out + Send + Sync>>,
@@ -241,6 +208,7 @@ impl_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12);
impl<In: 'static, Out: 'static> Callback<In, Out> {
/// Creates a new callback from the given function.
#[track_caller]
pub fn new<F>(fun: F) -> Self
where
F: Fn(In) -> Out + Send + Sync + 'static,
@@ -262,22 +230,94 @@ impl<In: 'static, Out: 'static> Callback<In, Out> {
}
}
#[doc(hidden)]
pub struct __IntoReactiveValueMarkerCallbackSingleParam;
#[doc(hidden)]
pub struct __IntoReactiveValueMarkerCallbackStrOutputToString;
impl<I, O, F>
IntoReactiveValue<
Callback<I, O>,
__IntoReactiveValueMarkerCallbackSingleParam,
> for F
where
F: Fn(I) -> O + Send + Sync + 'static,
{
#[track_caller]
fn into_reactive_value(self) -> Callback<I, O> {
Callback::new(self)
}
}
impl<I, O, F>
IntoReactiveValue<
UnsyncCallback<I, O>,
__IntoReactiveValueMarkerCallbackSingleParam,
> for F
where
F: Fn(I) -> O + 'static,
{
#[track_caller]
fn into_reactive_value(self) -> UnsyncCallback<I, O> {
UnsyncCallback::new(self)
}
}
impl<I, F>
IntoReactiveValue<
Callback<I, String>,
__IntoReactiveValueMarkerCallbackStrOutputToString,
> for F
where
F: Fn(I) -> &'static str + Send + Sync + 'static,
{
#[track_caller]
fn into_reactive_value(self) -> Callback<I, String> {
Callback::new(move |i| self(i).to_string())
}
}
impl<I, F>
IntoReactiveValue<
UnsyncCallback<I, String>,
__IntoReactiveValueMarkerCallbackStrOutputToString,
> for F
where
F: Fn(I) -> &'static str + 'static,
{
#[track_caller]
fn into_reactive_value(self) -> UnsyncCallback<I, String> {
UnsyncCallback::new(move |i| self(i).to_string())
}
}
#[cfg(test)]
mod tests {
use super::Callable;
use crate::callback::{Callback, UnsyncCallback};
use reactive_graph::traits::Dispose;
use crate::{
callback::{Callback, UnsyncCallback},
owner::Owner,
traits::Dispose,
IntoReactiveValue,
};
struct NoClone {}
#[test]
fn clone_callback() {
let owner = Owner::new();
owner.set();
let callback = Callback::new(move |_no_clone: NoClone| NoClone {});
let _cloned = callback;
}
#[test]
fn clone_unsync_callback() {
let owner = Owner::new();
owner.set();
let callback =
UnsyncCallback::new(move |_no_clone: NoClone| NoClone {});
let _cloned = callback;
@@ -285,20 +325,39 @@ mod tests {
#[test]
fn runback_from() {
let owner = Owner::new();
owner.set();
let _callback: Callback<(), String> = (|| "test").into();
let _callback: Callback<(i32, String), String> =
(|num, s| format!("{num} {s}")).into();
// Single params should work without needing the (foo,) tuple using IntoReactiveValue:
let _callback: Callback<usize, &'static str> =
(|_usize| "test").into_reactive_value();
let _callback: Callback<usize, String> =
(|_usize| "test").into_reactive_value();
}
#[test]
fn sync_callback_from() {
let owner = Owner::new();
owner.set();
let _callback: UnsyncCallback<(), String> = (|| "test").into();
let _callback: UnsyncCallback<(i32, String), String> =
(|num, s| format!("{num} {s}")).into();
// Single params should work without needing the (foo,) tuple using IntoReactiveValue:
let _callback: UnsyncCallback<usize, &'static str> =
(|_usize| "test").into_reactive_value();
let _callback: UnsyncCallback<usize, String> =
(|_usize| "test").into_reactive_value();
}
#[test]
fn sync_callback_try_run() {
let owner = Owner::new();
owner.set();
let callback = Callback::new(move |arg| arg);
assert_eq!(callback.try_run((0,)), Some((0,)));
callback.dispose();
@@ -307,6 +366,9 @@ mod tests {
#[test]
fn unsync_callback_try_run() {
let owner = Owner::new();
owner.set();
let callback = UnsyncCallback::new(move |arg| arg);
assert_eq!(callback.try_run((0,)), Some((0,)));
callback.dispose();
@@ -315,6 +377,9 @@ mod tests {
#[test]
fn callback_matches_same() {
let owner = Owner::new();
owner.set();
let callback1 = Callback::new(|x: i32| x * 2);
let callback2 = callback1;
assert!(callback1.matches(&callback2));
@@ -322,6 +387,9 @@ mod tests {
#[test]
fn callback_matches_different() {
let owner = Owner::new();
owner.set();
let callback1 = Callback::new(|x: i32| x * 2);
let callback2 = Callback::new(|x: i32| x + 1);
assert!(!callback1.matches(&callback2));
@@ -329,6 +397,9 @@ mod tests {
#[test]
fn unsync_callback_matches_same() {
let owner = Owner::new();
owner.set();
let callback1 = UnsyncCallback::new(|x: i32| x * 2);
let callback2 = callback1;
assert!(callback1.matches(&callback2));
@@ -336,6 +407,9 @@ mod tests {
#[test]
fn unsync_callback_matches_different() {
let owner = Owner::new();
owner.set();
let callback1 = UnsyncCallback::new(|x: i32| x * 2);
let callback2 = UnsyncCallback::new(|x: i32| x + 1);
assert!(!callback1.matches(&callback2));

View File

@@ -110,10 +110,12 @@ fn effect_base() -> (Receiver, Owner, Arc<RwLock<EffectInner>>) {
(rx, owner, inner)
}
#[cfg(debug_assertions)]
thread_local! {
static EFFECT_SCOPE_ACTIVE: AtomicBool = const { AtomicBool::new(false) };
}
#[cfg(debug_assertions)]
/// Returns whether the current thread is currently running an effect.
pub fn in_effect_scope() -> bool {
EFFECT_SCOPE_ACTIVE
@@ -123,14 +125,22 @@ pub fn in_effect_scope() -> bool {
/// Set a static to true whilst running the given function.
/// [`is_in_effect_scope`] will return true whilst the function is running.
fn run_in_effect_scope<T>(fun: impl FnOnce() -> T) -> T {
// For the theoretical nested case, set back to initial value rather than false:
let initial = EFFECT_SCOPE_ACTIVE
.with(|scope| scope.swap(true, std::sync::atomic::Ordering::Relaxed));
let result = fun();
EFFECT_SCOPE_ACTIVE.with(|scope| {
scope.store(initial, std::sync::atomic::Ordering::Relaxed)
});
result
#[cfg(debug_assertions)]
{
// For the theoretical nested case, set back to initial value rather than false:
let initial = EFFECT_SCOPE_ACTIVE.with(|scope| {
scope.swap(true, std::sync::atomic::Ordering::Relaxed)
});
let result = fun();
EFFECT_SCOPE_ACTIVE.with(|scope| {
scope.store(initial, std::sync::atomic::Ordering::Relaxed)
});
result
}
#[cfg(not(debug_assertions))]
{
fun()
}
}
impl<S> Effect<S>

View File

@@ -0,0 +1,67 @@
#[doc(hidden)]
pub struct __IntoReactiveValueMarkerBaseCase;
/// A helper trait that works like `Into<T>` but uses a marker generic
/// to allow more `From` implementations than would be allowed with just `Into<T>`.
pub trait IntoReactiveValue<T, M> {
/// Converts `self` into a `T`.
fn into_reactive_value(self) -> T;
}
// The base case, which allows anything which implements .into() to work:
impl<T, I> IntoReactiveValue<T, __IntoReactiveValueMarkerBaseCase> for I
where
I: Into<T>,
{
fn into_reactive_value(self) -> T {
self.into()
}
}
#[cfg(test)]
mod tests {
use crate::{
into_reactive_value::IntoReactiveValue,
owner::{LocalStorage, Owner},
traits::GetUntracked,
wrappers::read::Signal,
};
use typed_builder::TypedBuilder;
#[test]
fn test_into_signal_compiles() {
let owner = Owner::new();
owner.set();
#[cfg(not(feature = "nightly"))]
let _: Signal<usize> = (|| 2).into_reactive_value();
let _: Signal<usize, LocalStorage> = 2.into_reactive_value();
#[cfg(not(feature = "nightly"))]
let _: Signal<usize, LocalStorage> = (|| 2).into_reactive_value();
let _: Signal<String> = "str".into_reactive_value();
let _: Signal<String, LocalStorage> = "str".into_reactive_value();
#[derive(TypedBuilder)]
struct Foo {
#[builder(setter(
fn transform<M>(value: impl IntoReactiveValue<Signal<usize>, M>) {
value.into_reactive_value()
}
))]
sig: Signal<usize>,
}
assert_eq!(Foo::builder().sig(2).build().sig.get_untracked(), 2);
#[cfg(not(feature = "nightly"))]
assert_eq!(Foo::builder().sig(|| 2).build().sig.get_untracked(), 2);
assert_eq!(
Foo::builder()
.sig(Signal::stored(2))
.build()
.sig
.get_untracked(),
2
);
}
}

View File

@@ -90,6 +90,12 @@ pub mod traits;
pub mod transition;
pub mod wrappers;
mod into_reactive_value;
pub use into_reactive_value::*;
/// A standard way to wrap functions and closures to pass them to components.
pub mod callback;
use computed::ScopedFuture;
#[cfg(all(feature = "nightly", rustc_nightly))]
@@ -97,7 +103,9 @@ mod nightly;
/// Reexports frequently-used traits.
pub mod prelude {
pub use crate::{owner::FromLocal, traits::*};
pub use crate::{
into_reactive_value::IntoReactiveValue, owner::FromLocal, traits::*,
};
}
// TODO remove this, it's just useful while developing

View File

@@ -340,6 +340,8 @@ impl Owner {
}
/// Removes this from its state as the thread-local owner and drops it.
/// If there are other holders of this owner, it may not cleanup, if always cleaning up is required,
/// see [`Owner::unset_with_forced_cleanup`].
pub fn unset(self) {
OWNER.with_borrow_mut(|owner| {
if owner.as_ref().and_then(|n| n.upgrade()) == Some(self) {
@@ -348,6 +350,23 @@ impl Owner {
})
}
/// Removes this from its state as the thread-local owner and drops it.
/// Unlike [`Owner::unset`], this will always run cleanup on this owner,
/// even if there are other holders of this owner.
pub fn unset_with_forced_cleanup(self) {
OWNER.with_borrow_mut(|owner| {
if owner
.as_ref()
.and_then(|n| n.upgrade())
.map(|o| o == self)
.unwrap_or(false)
{
mem::take(owner);
}
});
self.cleanup();
}
/// Returns the current [`SharedContext`], if any.
#[cfg(feature = "hydration")]
pub fn current_shared_context(

View File

@@ -324,6 +324,22 @@ pub mod read {
}
}
impl<S> From<&'static str> for ArcSignal<String, S>
where
S: Storage<&'static str> + Storage<String>,
{
#[track_caller]
fn from(value: &'static str) -> Self {
Self {
inner: SignalTypes::Stored(ArcStoredValue::new(
value.to_string(),
)),
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: std::panic::Location::caller(),
}
}
}
impl<T, S> DefinedAt for ArcSignal<T, S>
where
S: Storage<T>,
@@ -1049,6 +1065,13 @@ pub mod read {
}
}
impl From<Signal<&'static str, LocalStorage>> for Signal<String, LocalStorage> {
#[track_caller]
fn from(value: Signal<&'static str, LocalStorage>) -> Self {
Signal::derive_local(move || value.read().to_string())
}
}
impl From<Signal<&'static str>> for Signal<String, LocalStorage> {
#[track_caller]
fn from(value: Signal<&'static str>) -> Self {
@@ -1077,6 +1100,15 @@ pub mod read {
}
}
impl From<Signal<Option<&'static str>, LocalStorage>>
for Signal<Option<String>, LocalStorage>
{
#[track_caller]
fn from(value: Signal<Option<&'static str>, LocalStorage>) -> Self {
Signal::derive_local(move || value.read().map(str::to_string))
}
}
impl From<Signal<Option<&'static str>>>
for Signal<Option<String>, LocalStorage>
{
@@ -1086,6 +1118,192 @@ pub mod read {
}
}
#[cfg(not(feature = "nightly"))]
#[doc(hidden)]
pub struct __IntoReactiveValueMarkerSignalFromReactiveClosure;
#[cfg(not(feature = "nightly"))]
#[doc(hidden)]
pub struct __IntoReactiveValueMarkerSignalStrOutputToString;
#[cfg(not(feature = "nightly"))]
#[doc(hidden)]
pub struct __IntoReactiveValueMarkerOptionalSignalFromReactiveClosureAlways;
#[cfg(not(feature = "nightly"))]
impl<T, F>
crate::IntoReactiveValue<
Signal<T, SyncStorage>,
__IntoReactiveValueMarkerSignalFromReactiveClosure,
> for F
where
T: Send + Sync + 'static,
F: Fn() -> T + Send + Sync + 'static,
{
fn into_reactive_value(self) -> Signal<T, SyncStorage> {
Signal::derive(self)
}
}
#[cfg(not(feature = "nightly"))]
impl<T, F>
crate::IntoReactiveValue<
ArcSignal<T, SyncStorage>,
__IntoReactiveValueMarkerSignalFromReactiveClosure,
> for F
where
T: Send + Sync + 'static,
F: Fn() -> T + Send + Sync + 'static,
{
fn into_reactive_value(self) -> ArcSignal<T, SyncStorage> {
ArcSignal::derive(self)
}
}
#[cfg(not(feature = "nightly"))]
impl<T, F>
crate::IntoReactiveValue<
Signal<T, LocalStorage>,
__IntoReactiveValueMarkerSignalFromReactiveClosure,
> for F
where
T: 'static,
F: Fn() -> T + 'static,
{
fn into_reactive_value(self) -> Signal<T, LocalStorage> {
Signal::derive_local(self)
}
}
#[cfg(not(feature = "nightly"))]
impl<T, F>
crate::IntoReactiveValue<
ArcSignal<T, LocalStorage>,
__IntoReactiveValueMarkerSignalFromReactiveClosure,
> for F
where
T: 'static,
F: Fn() -> T + 'static,
{
fn into_reactive_value(self) -> ArcSignal<T, LocalStorage> {
ArcSignal::derive_local(self)
}
}
#[cfg(not(feature = "nightly"))]
impl<F>
crate::IntoReactiveValue<
Signal<String, SyncStorage>,
__IntoReactiveValueMarkerSignalStrOutputToString,
> for F
where
F: Fn() -> &'static str + Send + Sync + 'static,
{
fn into_reactive_value(self) -> Signal<String, SyncStorage> {
Signal::derive(move || self().to_string())
}
}
#[cfg(not(feature = "nightly"))]
impl<F>
crate::IntoReactiveValue<
ArcSignal<String, SyncStorage>,
__IntoReactiveValueMarkerSignalStrOutputToString,
> for F
where
F: Fn() -> &'static str + Send + Sync + 'static,
{
fn into_reactive_value(self) -> ArcSignal<String, SyncStorage> {
ArcSignal::derive(move || self().to_string())
}
}
#[cfg(not(feature = "nightly"))]
impl<F>
crate::IntoReactiveValue<
Signal<String, LocalStorage>,
__IntoReactiveValueMarkerSignalStrOutputToString,
> for F
where
F: Fn() -> &'static str + 'static,
{
fn into_reactive_value(self) -> Signal<String, LocalStorage> {
Signal::derive_local(move || self().to_string())
}
}
#[cfg(not(feature = "nightly"))]
impl<F>
crate::IntoReactiveValue<
ArcSignal<String, LocalStorage>,
__IntoReactiveValueMarkerSignalStrOutputToString,
> for F
where
F: Fn() -> &'static str + 'static,
{
fn into_reactive_value(self) -> ArcSignal<String, LocalStorage> {
ArcSignal::derive_local(move || self().to_string())
}
}
#[cfg(not(feature = "nightly"))]
impl<T, F>
crate::IntoReactiveValue<
Signal<Option<T>, SyncStorage>,
__IntoReactiveValueMarkerOptionalSignalFromReactiveClosureAlways,
> for F
where
T: Send + Sync + 'static,
F: Fn() -> T + Send + Sync + 'static,
{
fn into_reactive_value(self) -> Signal<Option<T>, SyncStorage> {
Signal::derive(move || Some(self()))
}
}
#[cfg(not(feature = "nightly"))]
impl<T, F>
crate::IntoReactiveValue<
ArcSignal<Option<T>, SyncStorage>,
__IntoReactiveValueMarkerOptionalSignalFromReactiveClosureAlways,
> for F
where
T: Send + Sync + 'static,
F: Fn() -> T + Send + Sync + 'static,
{
fn into_reactive_value(self) -> ArcSignal<Option<T>, SyncStorage> {
ArcSignal::derive(move || Some(self()))
}
}
#[cfg(not(feature = "nightly"))]
impl<T, F>
crate::IntoReactiveValue<
Signal<Option<T>, LocalStorage>,
__IntoReactiveValueMarkerOptionalSignalFromReactiveClosureAlways,
> for F
where
T: 'static,
F: Fn() -> T + 'static,
{
fn into_reactive_value(self) -> Signal<Option<T>, LocalStorage> {
Signal::derive_local(move || Some(self()))
}
}
#[cfg(not(feature = "nightly"))]
impl<T, F>
crate::IntoReactiveValue<
ArcSignal<Option<T>, LocalStorage>,
__IntoReactiveValueMarkerOptionalSignalFromReactiveClosureAlways,
> for F
where
T: 'static,
F: Fn() -> T + 'static,
{
fn into_reactive_value(self) -> ArcSignal<Option<T>, LocalStorage> {
ArcSignal::derive_local(move || Some(self()))
}
}
#[allow(deprecated)]
impl<T> From<MaybeSignal<T>> for Signal<T>
where

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router"
version = "0.8.9"
version = "0.8.10"
authors = ["Greg Johnston", "Ben Wishovich"]
license = "MIT"
readme = "../README.md"

View File

@@ -105,7 +105,7 @@
//! move || params.read().get("id").unwrap_or_default(),
//! move |id| contact_data(id)
//! );
//! todo!()
//! // ... return some view
//! }
//! ```
//!

View File

@@ -368,8 +368,8 @@ where
ev.prevent_default();
let to = path_name
+ if url.search.is_empty() { "" } else { "?" }
+ &Url::unescape(&url.search)
+ &Url::unescape(&url.hash);
+ &url.search
+ &url.hash;
let state = Reflect::get(&a, &JsValue::from_str("state"))
.ok()
.and_then(|value| {

View File

@@ -4,7 +4,6 @@ macro_rules! tuples {
($first:ident => $($ty:ident),*) => {
impl<$first, $($ty),*> PossibleRouteMatch for ($first, $($ty,)*)
where
Self: core::fmt::Debug,
$first: PossibleRouteMatch,
$($ty: PossibleRouteMatch),*,
{

View File

@@ -369,7 +369,7 @@ impl ResolvedStaticPath {
eprintln!("{e}");
}
}
owner.unset();
owner.unset_with_forced_cleanup();
}
}
});

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router_macro"
version = "0.8.5"
version = "0.8.6"
authors = ["Greg Johnston", "Ben Wishovich"]
license = "MIT"
readme = "../README.md"

View File

@@ -7,7 +7,7 @@
use proc_macro::{TokenStream, TokenTree};
use proc_macro2::Span;
use proc_macro_error2::{abort, proc_macro_error, set_dummy};
use quote::{quote, ToTokens};
use quote::{format_ident, quote, ToTokens};
use syn::{
spanned::Spanned, FnArg, Ident, ImplItem, ItemImpl, Path, Type, TypePath,
};
@@ -267,10 +267,7 @@ fn lazy_route_impl(
};
let lazy_view_ident =
Ident::new(&format!("__{ty_name_to_snake}_View"), im.self_ty.span());
let preload_lazy_view_ident = Ident::new(
&format!("__preload_{lazy_view_ident}"),
lazy_view_ident.span(),
);
let preload_ident = format_ident!("__preload_{lazy_view_ident}");
im.items.push(
syn::parse::<ImplItem>(
@@ -280,7 +277,7 @@ fn lazy_route_impl(
// we don't split routes for wasm32 ssr
// but we don't require a `hydrate`/`csr` feature on leptos_router
#[cfg(target_arch = "wasm32")]
#preload_lazy_view_ident().await;
#preload_ident().await;
}
}
.into(),

View File

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

View File

@@ -57,6 +57,10 @@ where
_style: &mut String,
_inner_html: &mut String,
) {
// If this is a class="..." attribute (not class:name=value), clear previous value
if self.class.should_overwrite() {
class.clear();
}
class.push(' ');
self.class.to_html(class);
}
@@ -156,6 +160,12 @@ pub trait IntoClass: Send {
/// Renders the class to HTML.
fn to_html(self, class: &mut String);
/// Whether this class attribute should overwrite previous class values.
/// Returns `true` for `class="..."` attributes, `false` for `class:name=value` directives.
fn should_overwrite(&self) -> bool {
false
}
/// Renders the class to HTML for a `<template>`.
#[allow(unused)] // it's used with `nightly` feature
fn to_template(class: &mut String) {}
@@ -289,6 +299,10 @@ impl IntoClass for &str {
class.push_str(self);
}
fn should_overwrite(&self) -> bool {
true
}
fn hydrate<const FROM_SERVER: bool>(
self,
el: &crate::renderer::types::Element,
@@ -346,6 +360,10 @@ impl IntoClass for Cow<'_, str> {
IntoClass::to_html(&*self, class);
}
fn should_overwrite(&self) -> bool {
true
}
fn hydrate<const FROM_SERVER: bool>(
self,
el: &crate::renderer::types::Element,
@@ -403,6 +421,10 @@ impl IntoClass for String {
IntoClass::to_html(self.as_str(), class);
}
fn should_overwrite(&self) -> bool {
true
}
fn hydrate<const FROM_SERVER: bool>(
self,
el: &crate::renderer::types::Element,
@@ -460,6 +482,10 @@ impl IntoClass for Arc<str> {
IntoClass::to_html(self.as_ref(), class);
}
fn should_overwrite(&self) -> bool {
true
}
fn hydrate<const FROM_SERVER: bool>(
self,
el: &crate::renderer::types::Element,

View File

@@ -47,11 +47,13 @@ pub fn directive<T, P, D>(handler: D, param: P) -> Directive<T, D, P>
where
D: IntoDirective<T, P>,
{
Directive(Some(SendWrapper::new(DirectiveInner {
handler,
param,
t: PhantomData,
})))
Directive((!cfg!(feature = "ssr")).then(|| {
SendWrapper::new(DirectiveInner {
handler,
param,
t: PhantomData,
})
}))
}
/// Custom logic that runs in the browser when the element is created or hydrated.
@@ -151,13 +153,7 @@ where
Directive(inner)
}
fn dry_resolve(&mut self) {
// dry_resolve() only runs during SSR, and we should use it to
// synchronously remove and drop the SendWrapper value
// we don't need this value during SSR and leaving it here could drop it
// from a different thread
self.0.take();
}
fn dry_resolve(&mut self) {}
async fn resolve(self) -> Self::AsyncOutput {
self

View File

@@ -317,6 +317,26 @@ where
type State = ElementState<At::State, Ch::State>;
fn rebuild(self, state: &mut Self::State) {
// check whether the tag is the same, for custom elements
// because this is const `false` for all other element types,
// the compiler should be able to optimize it out
if E::TAG.is_empty() {
// see https://github.com/leptos-rs/leptos/issues/4412
let new_tag = self.tag.tag();
// this is not particularly efficient, but it saves us from
// having to keep track of the tag name for every element state
let old_tag = state.el.tag_name();
if new_tag != old_tag {
let mut new_state = self.build();
state.insert_before_this(&mut new_state);
state.unmount();
*state = new_state;
return;
}
}
// rebuild attributes and children for any element
let ElementState {
attrs, children, ..
} = state;

View File

@@ -113,7 +113,7 @@ where
event,
#[cfg(feature = "reactive_graph")]
owner: reactive_graph::owner::Owner::current().unwrap_or_default(),
cb: Some(SendWrapper::new(cb)),
cb: (!cfg!(feature = "ssr")).then(|| SendWrapper::new(cb)),
}
}
@@ -352,13 +352,7 @@ where
}
}
fn dry_resolve(&mut self) {
// dry_resolve() only runs during SSR, and we should use it to
// synchronously remove and drop the SendWrapper value
// we don't need this value during SSR and leaving it here could drop it
// from a different thread
self.cb.take();
}
fn dry_resolve(&mut self) {}
async fn resolve(self) -> Self::AsyncOutput {
self

View File

@@ -22,7 +22,7 @@ where
{
Property {
key,
value: Some(SendWrapper::new(value)),
value: (!cfg!(feature = "ssr")).then(|| SendWrapper::new(value)),
}
}
@@ -115,13 +115,7 @@ where
}
}
fn dry_resolve(&mut self) {
// dry_resolve() only runs during SSR, and we should use it to
// synchronously remove and drop the SendWrapper value
// we don't need this value during SSR and leaving it here could drop it
// from a different thread
self.value.take();
}
fn dry_resolve(&mut self) {}
async fn resolve(self) -> Self::AsyncOutput {
self

View File

@@ -7,6 +7,9 @@ use std::cell::Cell;
use std::{cell::RefCell, panic::Location, rc::Rc};
use web_sys::{Comment, Element, Node, Text};
#[cfg(feature = "mark_branches")]
const COMMENT_NODE: u16 = 8;
/// Hydration works by walking over the DOM, adding interactivity as needed.
///
/// This cursor tracks the location in the DOM that is currently being hydrated. Each that type
@@ -43,13 +46,27 @@ where
///
/// Does nothing if there is no child.
pub fn child(&self) {
//crate::log("advancing to next child of ");
//Rndr::log_node(&self.current());
let mut inner = self.0.borrow_mut();
if let Some(node) = Rndr::first_child(&inner) {
*inner = node;
}
//drop(inner);
#[cfg(feature = "mark_branches")]
{
while inner.node_type() == COMMENT_NODE {
if let Some(content) = inner.text_content() {
if content.starts_with("bo") || content.starts_with("bc") {
if let Some(sibling) = Rndr::next_sibling(&inner) {
*inner = sibling;
continue;
}
}
}
break;
}
}
// //drop(inner);
//crate::log(">> which is ");
//Rndr::log_node(&self.current());
}
@@ -58,12 +75,25 @@ where
///
/// Does nothing if there is no sibling.
pub fn sibling(&self) {
//crate::log("advancing to next sibling of ");
//Rndr::log_node(&self.current());
let mut inner = self.0.borrow_mut();
if let Some(node) = Rndr::next_sibling(&inner) {
*inner = node;
}
#[cfg(feature = "mark_branches")]
{
while inner.node_type() == COMMENT_NODE {
if let Some(content) = inner.text_content() {
if content.starts_with("bo") || content.starts_with("bc") {
if let Some(sibling) = Rndr::next_sibling(&inner) {
*inner = sibling;
continue;
}
}
}
break;
}
}
//drop(inner);
//crate::log(">> which is ");
//Rndr::log_node(&self.current());

View File

@@ -575,15 +575,7 @@ impl RenderHtml for AnyView {
#[cfg(feature = "hydrate")]
{
if FROM_SERVER {
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
let state =
(self.hydrate_from_server)(self.value, cursor, position);
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
state
(self.hydrate_from_server)(self.value, cursor, position)
} else {
panic!(
"hydrating AnyView from inside a ViewTemplate is not \
@@ -609,14 +601,8 @@ impl RenderHtml for AnyView {
) -> Self::State {
#[cfg(feature = "hydrate")]
{
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
let state =
(self.hydrate_async)(self.value, cursor, position).await;
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
state
}
#[cfg(not(feature = "hydrate"))]

View File

@@ -411,21 +411,14 @@ where
cursor: &Cursor,
position: &PositionState,
) -> Self::State {
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
let state = match self {
match self {
Either::Left(left) => {
Either::Left(left.hydrate::<FROM_SERVER>(cursor, position))
}
Either::Right(right) => {
Either::Right(right.hydrate::<FROM_SERVER>(cursor, position))
}
};
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
state
}
async fn hydrate_async(
@@ -433,21 +426,14 @@ where
cursor: &Cursor,
position: &PositionState,
) -> Self::State {
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
let state = match self {
match self {
Either::Left(left) => {
Either::Left(left.hydrate_async(cursor, position).await)
}
Either::Right(right) => {
Either::Right(right.hydrate_async(cursor, position).await)
}
};
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
state
}
fn into_owned(self) -> Self::Owned {
@@ -973,17 +959,11 @@ macro_rules! tuples {
cursor: &Cursor,
position: &PositionState,
) -> Self::State {
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
let state = match self {
$([<EitherOf $num>]::$ty(this) => {
[<EitherOf $num>]::$ty(this.hydrate::<FROM_SERVER>(cursor, position))
})*
};
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
Self::State { state }
}
@@ -993,17 +973,11 @@ macro_rules! tuples {
cursor: &Cursor,
position: &PositionState,
) -> Self::State {
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
let state = match self {
$([<EitherOf $num>]::$ty(this) => {
[<EitherOf $num>]::$ty(this.hydrate_async(cursor, position).await)
})*
};
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
Self::State { state }
}

View File

@@ -322,10 +322,6 @@ where
cursor: &Cursor,
position: &PositionState,
) -> Self::State {
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
// get parent and position
let current = cursor.current();
let parent = if position.get() == Position::FirstChild {
@@ -346,22 +342,12 @@ where
for (index, item) in items.enumerate() {
hashed_items.insert((self.key_fn)(&item));
let (set_index, view) = (self.view_fn)(index, item);
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
let item = view.hydrate::<FROM_SERVER>(cursor, position);
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
rendered_items.push(Some((set_index, item)));
}
let marker = cursor.next_placeholder(position);
position.set(Position::NextChild);
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
KeyedState {
parent: Some(parent),
marker,
@@ -375,10 +361,6 @@ where
cursor: &Cursor,
position: &PositionState,
) -> Self::State {
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
// get parent and position
let current = cursor.current();
let parent = if position.get() == Position::FirstChild {
@@ -399,22 +381,12 @@ where
for (index, item) in items.enumerate() {
hashed_items.insert((self.key_fn)(&item));
let (set_index, view) = (self.view_fn)(index, item);
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
let item = view.hydrate_async(cursor, position).await;
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
rendered_items.push(Some((set_index, item)));
}
let marker = cursor.next_placeholder(position);
position.set(Position::NextChild);
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
KeyedState {
parent: Some(parent),
marker,
@@ -642,18 +614,13 @@ struct DiffOpRemove {
at: usize,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
enum DiffOpAddMode {
#[default]
Normal,
Append,
}
impl Default for DiffOpAddMode {
fn default() -> Self {
Self::Normal
}
}
fn apply_diff<T, VFS, V>(
parent: Option<&crate::renderer::types::Element>,
marker: &crate::renderer::types::Placeholder,

View File

@@ -1,17 +0,0 @@
[package]
name = "wasm_split_helpers"
version = "0.1.2"
authors = ["Greg Johnston"]
license = "MIT"
readme = "README.md"
repository = "https://github.com/leptos-rs/leptos"
description = "Tools to support code-splitting and lazy loading for WebAssembly (WASM) binaries."
rust-version.workspace = true
edition.workspace = true
[dependencies]
async-once-cell = { default-features = true, workspace = true, features = [
"std",
] }
wasm_split_macros.workspace = true
or_poisoned.workspace = true

View File

@@ -1 +0,0 @@
extend = { path = "../cargo-make/main.toml" }

View File

@@ -1,9 +0,0 @@
# `wasm_split_helpers`
This crate provides functions that are used by the `wasm_split_macros` crate, which allows you to indicate that certain functions are appropriate split points for lazy-loaded code.
A build tool that supports this approach (like `cargo-leptos`) can then split a WebAssembly (WASM) binary into multiple chunks, which will be lazy-loaded when a split function is called.
This crate was adapted from an original prototype, which you can find [here](https://github.com/jbms/wasm-split-prototype), with an in-depth description of the approach [here](https://github.com/rustwasm/wasm-bindgen/issues/3939).
This functionality is provided in Leptos by the `#[lazy]` and `#[lazy_route]` macros.

View File

@@ -1,107 +0,0 @@
use std::{
ffi::c_void,
future::Future,
pin::Pin,
sync::{Arc, Mutex},
task::{Context, Poll, Waker},
};
pub type LoadCallbackFn = unsafe extern "C" fn(*const c_void, bool) -> ();
pub type LoadFn = unsafe extern "C" fn(LoadCallbackFn, *const c_void) -> ();
type Lazy = async_once_cell::Lazy<Option<()>, SplitLoaderFuture>;
use or_poisoned::OrPoisoned;
pub use wasm_split_macros::wasm_split;
pub struct LazySplitLoader {
lazy: Pin<Arc<Lazy>>,
}
impl LazySplitLoader {
pub fn new(load: LoadFn) -> Self {
Self {
lazy: Arc::pin(Lazy::new(SplitLoaderFuture::new(
SplitLoader::new(load),
))),
}
}
}
pub async fn ensure_loaded(
loader: &'static std::thread::LocalKey<LazySplitLoader>,
) -> Option<()> {
*loader.with(|inner| inner.lazy.clone()).as_ref().await
}
#[derive(Clone, Copy, Debug)]
enum SplitLoaderState {
Deferred(LoadFn),
Pending,
Completed(Option<()>),
}
struct SplitLoader {
state: Mutex<SplitLoaderState>,
waker: Mutex<Option<Waker>>,
}
impl SplitLoader {
fn new(load: LoadFn) -> Arc<Self> {
Arc::new(SplitLoader {
state: Mutex::new(SplitLoaderState::Deferred(load)),
waker: Mutex::new(None),
})
}
fn complete(&self, value: bool) {
*self.state.lock().or_poisoned() =
SplitLoaderState::Completed(if value { Some(()) } else { None });
if let Some(waker) = self.waker.lock().or_poisoned().take() {
waker.wake();
}
}
}
struct SplitLoaderFuture {
loader: Arc<SplitLoader>,
}
impl SplitLoaderFuture {
fn new(loader: Arc<SplitLoader>) -> Self {
SplitLoaderFuture { loader }
}
}
impl Future for SplitLoaderFuture {
type Output = Option<()>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<()>> {
let mut loader = self.loader.state.lock().or_poisoned();
match *loader {
SplitLoaderState::Deferred(load) => {
*loader = SplitLoaderState::Pending;
*self.loader.waker.lock().or_poisoned() =
Some(cx.waker().clone());
unsafe {
load(
load_callback,
Arc::<SplitLoader>::into_raw(self.loader.clone())
as *const c_void,
)
};
Poll::Pending
}
SplitLoaderState::Pending => {
*self.loader.waker.lock().or_poisoned() =
Some(cx.waker().clone());
Poll::Pending
}
SplitLoaderState::Completed(value) => Poll::Ready(value),
}
}
}
unsafe extern "C" fn load_callback(loader: *const c_void, success: bool) {
unsafe { Arc::from_raw(loader as *const SplitLoader) }.complete(success);
}

View File

@@ -1,21 +0,0 @@
[package]
name = "wasm_split_macros"
version = "0.1.3"
authors = ["Greg Johnston"]
license = "MIT"
readme = "README.md"
repository = "https://github.com/leptos-rs/leptos"
description = "Tools to support code-splitting and lazy loading for WebAssembly (WASM) binaries."
rust-version.workspace = true
edition.workspace = true
[dependencies]
base16 = { workspace = true, default-features = true }
digest = { workspace = true, default-features = true }
quote = { workspace = true, default-features = true }
sha2 = { workspace = true, default-features = true }
syn = { workspace = true, default-features = true }
wasm-bindgen = { workspace = true, default-features = true }
[lib]
proc-macro = true

View File

@@ -1 +0,0 @@
extend = { path = "../cargo-make/main.toml" }

View File

@@ -1,9 +0,0 @@
# `wasm_split_macros`
This crate provides macros that are used along with the `wasm_split_helpers` crate, which allows you to indicate that certain functions are appropriate split points for lazy-loaded code.
A build tool that supports this approach (like `cargo-leptos`) can then split a WebAssembly (WASM) binary into multiple chunks, which will be lazy-loaded when a split function is called.
This crate was adapted from an original prototype, which you can find [here](https://github.com/jbms/wasm-split-prototype), with an in-depth description of the approach [here](https://github.com/rustwasm/wasm-bindgen/issues/3939).
This functionality is provided in Leptos by the `#[lazy]` and `#[lazy_route]` macros.

View File

@@ -1,172 +0,0 @@
use digest::Digest;
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{
parse,
parse::{Parse, ParseStream},
parse_macro_input,
token::Comma,
Ident, ItemFn, Path, Result, ReturnType, Signature,
};
struct WasmSplitArgs {
module_ident: Ident,
_comma: Option<Comma>,
send_wrapper_path: Option<Path>,
}
impl Parse for WasmSplitArgs {
fn parse(input: ParseStream) -> Result<Self> {
let module_ident = input.parse()?;
let _comma = input.parse().ok();
let send_wrapper_path = input.parse().ok();
Ok(Self {
module_ident,
_comma,
send_wrapper_path,
})
}
}
#[proc_macro_attribute]
pub fn wasm_split(args: TokenStream, input: TokenStream) -> TokenStream {
let WasmSplitArgs {
module_ident,
send_wrapper_path,
..
} = parse_macro_input!(args);
let item_fn = parse_macro_input!(input as ItemFn);
let name = &item_fn.sig.ident;
let preload_name =
Ident::new(&format!("__preload_{}", item_fn.sig.ident), name.span());
let unique_identifier = base16::encode_lower(
&sha2::Sha256::digest(format!("{name} {span:?}", span = name.span()))
[..16],
);
let load_module_ident = format_ident!("__wasm_split_load_{module_ident}");
let split_loader_ident =
format_ident!("__wasm_split_loader_{unique_identifier}");
let impl_import_ident = format_ident!(
"__wasm_split_00{module_ident}00_import_{unique_identifier}_{name}"
);
let impl_export_ident = format_ident!(
"__wasm_split_00{module_ident}00_export_{unique_identifier}_{name}"
);
let mut import_sig = Signature {
ident: impl_import_ident.clone(),
asyncness: None,
..item_fn.sig.clone()
};
let mut export_sig = Signature {
ident: impl_export_ident.clone(),
asyncness: None,
..item_fn.sig.clone()
};
let was_async = item_fn.sig.asyncness.is_some();
if was_async {
let ty = match &item_fn.sig.output {
ReturnType::Default => quote! { () },
ReturnType::Type(_, ty) => quote! { #ty },
};
let async_output = parse::<ReturnType>(
quote! {
-> std::pin::Pin<Box<dyn std::future::Future<Output = #ty> + Send + Sync>>
}
.into(),
)
.unwrap();
export_sig.output = async_output.clone();
import_sig.output = async_output;
}
let wrapper_pub = item_fn.vis;
let mut wrapper_sig = item_fn.sig;
wrapper_sig.asyncness = Some(Default::default());
let mut args = Vec::new();
for (i, param) in wrapper_sig.inputs.iter_mut().enumerate() {
match param {
syn::FnArg::Typed(pat_type) => {
let param_ident = format_ident!("__wasm_split_arg_{i}");
args.push(param_ident.clone());
pat_type.pat = Box::new(syn::Pat::Ident(syn::PatIdent {
attrs: vec![],
by_ref: None,
mutability: None,
ident: param_ident,
subpat: None,
}));
}
syn::FnArg::Receiver(_) => {
args.push(format_ident!("self"));
}
}
}
let attrs = item_fn.attrs;
let stmts = &item_fn.block.stmts;
let body = if was_async {
if let Some(send_wrapper_path) = send_wrapper_path {
quote! {
Box::pin(#send_wrapper_path::SendWrapper::new(async move {
#(#stmts)*
}))
}
} else {
quote! {
Box::pin(async move {
#(#stmts)*
})
}
}
} else {
quote! { #(#stmts)* }
};
let await_result = was_async.then(|| quote! { .await });
quote! {
thread_local! {
static #split_loader_ident: ::leptos::wasm_split_helpers::LazySplitLoader = ::leptos::wasm_split_helpers::LazySplitLoader::new(#load_module_ident);
}
#[link(wasm_import_module = "/pkg/__wasm_split.______________________.js")]
extern "C" {
#[no_mangle]
fn #load_module_ident (callback: unsafe extern "C" fn(*const ::std::ffi::c_void, bool), data: *const ::std::ffi::c_void) -> ();
#[allow(improper_ctypes)]
#[no_mangle]
#import_sig;
}
#[allow(non_snake_case)]
#(#attrs)*
#wrapper_pub #wrapper_sig {
#(#attrs)*
#[allow(improper_ctypes_definitions)]
#[allow(non_snake_case)]
#[no_mangle]
pub extern "C" #export_sig {
#body
}
::leptos::wasm_split_helpers::ensure_loaded(&#split_loader_ident).await.unwrap();
unsafe { #impl_import_ident( #(#args),* ) } #await_result
}
#[doc(hidden)]
#[allow(non_snake_case)]
#wrapper_pub async fn #preload_name() {
::leptos::wasm_split_helpers::ensure_loaded(&#split_loader_ident).await.unwrap();
}
}
.into()
}