From feb897f1250fde7dd4cd39ade2ab1cc2cde24715 Mon Sep 17 00:00:00 2001 From: maccesch Date: Thu, 21 Aug 2025 17:00:46 +0200 Subject: [PATCH 01/91] made accept Signals in addition to closures --- leptos/src/show.rs | 93 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 89 insertions(+), 4 deletions(-) diff --git a/leptos/src/show.rs b/leptos/src/show.rs index 7cabb643c0..80cdee452f 100644 --- a/leptos/src/show.rs +++ b/leptos/src/show.rs @@ -1,26 +1,70 @@ use crate::{ children::{TypedChildrenFn, ViewFn}, + prelude::{FunctionMarker, SignalMarker}, IntoView, }; use leptos_macro::component; use reactive_graph::{computed::ArcMemo, traits::Get}; +use std::{marker::PhantomData, sync::Arc}; use tachys::either::Either; +/// Shows its children whenever the condition `when` prop is `true`. +/// Otherwise it renders the `fallback` prop, which defaults to the empty view. +/// +/// The prop `when` can be a closure that returns a bool, a signal of type bool, or a boolean value. +/// +/// ## Usage +/// +/// ``` +/// # use leptos::prelude::*; +/// # +/// # #[component] +/// # pub fn Demo() -> impl IntoView { +/// let (condition, set_condition) = signal(true); +/// +/// view! { +/// +///

"Hello, world!"

+///
+/// } +/// # } +/// ``` +/// +/// Or with a closure as the `when` condition: +/// +/// ``` +/// # use leptos::prelude::*; +/// # +/// # #[component] +/// # pub fn Demo() -> impl IntoView { +/// let (condition, set_condition) = signal(true); +/// +/// view! { +/// +///

"Hello, world!"

+///
+/// } +/// # } +/// ``` #[component] -pub fn Show( +pub fn Show( /// The children will be shown whenever the condition in the `when` closure returns `true`. children: TypedChildrenFn, /// A closure that returns a bool that determines whether this thing runs - when: W, + when: impl IntoCondition, /// A closure that returns what gets rendered if the when statement is false. By default this is the empty view. #[prop(optional, into)] fallback: ViewFn, + + /// Marker for generic parameters. Ignore this. + #[prop(optional)] + _marker: PhantomData, ) -> impl IntoView where - W: Fn() -> bool + Send + Sync + 'static, C: IntoView + 'static, { - let memoized_when = ArcMemo::new(move |_| when()); + let when = when.into_condition(); + let memoized_when = ArcMemo::new(move |_| when.run()); let children = children.into_inner(); move || match memoized_when.get() { @@ -28,3 +72,44 @@ where false => Either::Right(fallback.run()), } } + +/// A closure that returns a bool. Can be converted from a closure, a signal, or a boolean value. +pub struct Condition(Arc bool + Send + Sync + 'static>); + +impl Condition { + /// Evaluates the condition and returns its result. + pub fn run(&self) -> bool { + (self.0)() + } +} + +/// Trait to convert various types into a `Condition`. +/// Implemented for closures, signals, and boolean values. +pub trait IntoCondition { + /// Does the conversion + fn into_condition(self) -> Condition; +} + +impl IntoCondition for S +where + S: Get + Send + Sync + 'static, +{ + fn into_condition(self) -> Condition { + Condition(Arc::new(move || self.get())) + } +} + +impl IntoCondition for F +where + F: Fn() -> bool + Send + Sync + 'static, +{ + fn into_condition(self) -> Condition { + Condition(Arc::new(self)) + } +} + +impl IntoCondition for Condition { + fn into_condition(self) -> Condition { + self + } +} From fde5e68c087fec06b2d482f1d20b812ef829388a Mon Sep 17 00:00:00 2001 From: maccesch Date: Mon, 25 Aug 2025 17:49:43 +0200 Subject: [PATCH 02/91] added flexible when to AnimatedShow as well --- leptos/src/animated_show.rs | 47 ++++++++++++++++++++++++------------- leptos/src/show.rs | 3 ++- 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/leptos/src/animated_show.rs b/leptos/src/animated_show.rs index 5d8feff7d5..30ae021101 100644 --- a/leptos/src/animated_show.rs +++ b/leptos/src/animated_show.rs @@ -1,14 +1,18 @@ -use crate::{children::ChildrenFn, component, control_flow::Show, IntoView}; +use crate::{ + children::ChildrenFn, component, control_flow::Show, show::IntoCondition, + IntoView, +}; use core::time::Duration; use leptos_dom::helpers::TimeoutHandle; use leptos_macro::view; use reactive_graph::{ + diagnostics::SpecialNonReactiveZone, effect::RenderEffect, owner::{on_cleanup, StoredValue}, signal::RwSignal, - traits::{Get, GetUntracked, GetValue, Set, SetValue}, - wrappers::read::Signal, + traits::{Get, GetValue, Set, SetValue}, }; +use std::marker::PhantomData; use tachys::prelude::*; /// A component that will show its children when the `when` condition is `true`. @@ -46,14 +50,16 @@ use tachys::prelude::*; /// } /// # } /// ``` +/// +/// Please note, that unlike `Show`, `AnimatedShow` does not support a `fallback` prop. #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))] #[component] -pub fn AnimatedShow( +pub fn AnimatedShow( /// The components Show wraps children: ChildrenFn, - /// If the component should show or not - #[prop(into)] - when: Signal, + /// When true the children are shown. + /// It accepts a closure that returns a boolean value as well as a boolean signal or plain boolean value. + when: impl IntoCondition, /// Optional CSS class to apply if `when == true` #[prop(optional)] show_class: &'static str, @@ -62,17 +68,26 @@ pub fn AnimatedShow( hide_class: &'static str, /// The timeout after which the component will be unmounted if `when == false` hide_delay: Duration, + + /// Marker for generic parameters. Ignore this. + #[prop(optional)] + _marker: PhantomData, ) -> impl IntoView { + let when = when.into_condition(); + + // Silence warnings about using signals in non-reactive contexts. + #[cfg(debug_assertions)] + let z = SpecialNonReactiveZone::enter(); + let handle: StoredValue> = StoredValue::new(None); - let cls = RwSignal::new(if when.get_untracked() { - show_class - } else { - hide_class - }); - let show = RwSignal::new(when.get_untracked()); + let cls = RwSignal::new(if when.run() { show_class } else { hide_class }); + let show = RwSignal::new(when.run()); + + #[cfg(debug_assertions)] + drop(z); let eff = RenderEffect::new(move |_| { - if when.get() { + if when.run() { // clear any possibly active timer if let Some(h) = handle.get_value() { h.clear(); @@ -100,8 +115,8 @@ pub fn AnimatedShow( }); view! { - -
{children()}
+ +
{children()}
} } diff --git a/leptos/src/show.rs b/leptos/src/show.rs index 80cdee452f..fec7288052 100644 --- a/leptos/src/show.rs +++ b/leptos/src/show.rs @@ -50,7 +50,8 @@ use tachys::either::Either; pub fn Show( /// The children will be shown whenever the condition in the `when` closure returns `true`. children: TypedChildrenFn, - /// A closure that returns a bool that determines whether this thing runs + /// When true the children are shown, otherwise the fallback. + /// It accepts a closure that returns a boolean value as well as a boolean signal or plain boolean value. when: impl IntoCondition, /// A closure that returns what gets rendered if the when statement is false. By default this is the empty view. #[prop(optional, into)] From 0dacc041b4b157d298d01f2632c9bb384d0d0260 Mon Sep 17 00:00:00 2001 From: maccesch Date: Mon, 25 Aug 2025 18:06:40 +0200 Subject: [PATCH 03/91] chore: clippy --- leptos/src/animated_show.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/leptos/src/animated_show.rs b/leptos/src/animated_show.rs index 30ae021101..d91924755d 100644 --- a/leptos/src/animated_show.rs +++ b/leptos/src/animated_show.rs @@ -10,7 +10,7 @@ use reactive_graph::{ effect::RenderEffect, owner::{on_cleanup, StoredValue}, signal::RwSignal, - traits::{Get, GetValue, Set, SetValue}, + traits::{GetValue, Set, SetValue}, }; use std::marker::PhantomData; use tachys::prelude::*; From 34c975840510ca1128a31eb440b450f2ceea2e9e Mon Sep 17 00:00:00 2001 From: maccesch Date: Wed, 27 Aug 2025 15:32:21 +0200 Subject: [PATCH 04/91] fixed new and conditions with nightly. Also added to some appropriate places in examples. --- examples/axum_js_ssr/src/app.rs | 1219 +++++++++++------ examples/hackernews/src/routes/story.rs | 27 +- .../hackernews_axum/src/routes/stories.rs | 126 +- leptos/src/show.rs | 5 +- leptos/src/show_let.rs | 2 + 5 files changed, 852 insertions(+), 527 deletions(-) diff --git a/examples/axum_js_ssr/src/app.rs b/examples/axum_js_ssr/src/app.rs index b4cbded87e..3c1661e0c3 100644 --- a/examples/axum_js_ssr/src/app.rs +++ b/examples/axum_js_ssr/src/app.rs @@ -14,14 +14,14 @@ pub fn shell(options: LeptosOptions) -> impl IntoView { - - - - - + + + + + - + } @@ -34,38 +34,71 @@ pub fn App() -> impl IntoView { let fallback = || view! { "Page not found." }.into_view(); view! { - - - <Meta name="color-scheme" content="dark light"/> + <Stylesheet id="leptos" href="/pkg/axum_js_ssr.css" /> + <Title text="Leptos JavaScript Integration Demo with SSR in Axum" /> + <Meta name="color-scheme" content="dark light" /> <Router> <nav> - <A attr:class="section" href="/">"Introduction (home)"</A> - <A attr:class="example" href="/naive">"Naive "<code>"<script>"</code> - <small>"truly naive to start off"</small></A> - <A attr:class="example" href="/naive-alt">"Leptos "<code>"<Script>"</code> - <small>"naively using load event"</small></A> - <A attr:class="example" href="/naive-hook">"Leptos "<code>"<Script>"</code> - <small>"... correcting placement"</small></A> - <A attr:class="example" href="/naive-fallback">"Leptos "<code>"<Script>"</code> - <small>"... with fallback"</small></A> - <A attr:class="example" href="/signal-effect-script">"Leptos Signal + Effect" - <small>"an idiomatic Leptos solution"</small></A> - <A attr:class="subexample section" href="/custom-event">"Hydrated Event" - <small>"using "<code>"js_sys"</code>"/"<code>"web_sys"</code></small></A> - <A attr:class="example" href="/wasm-bindgen-naive">"Using "<code>"wasm-bindgen"</code> - <small>"naively to start with"</small></A> - <A attr:class="example" href="/wasm-bindgen-event">"Using "<code>"wasm-bindgen"</code> - <small>"overcomplication with events"</small></A> - <A attr:class="example" href="/wasm-bindgen-effect">"Using "<code>"wasm-bindgen"</code> - <small>"lazily delay DOM manipulation"</small></A> - <A attr:class="example" href="/wasm-bindgen-direct">"Using "<code>"wasm-bindgen"</code> - <small>"without DOM manipulation"</small></A> + <A attr:class="section" href="/"> + "Introduction (home)" + </A> + <A attr:class="example" href="/naive"> + "Naive " + <code>"<script>"</code> + <small>"truly naive to start off"</small> + </A> + <A attr:class="example" href="/naive-alt"> + "Leptos " + <code>"<Script>"</code> + <small>"naively using load event"</small> + </A> + <A attr:class="example" href="/naive-hook"> + "Leptos " + <code>"<Script>"</code> + <small>"... correcting placement"</small> + </A> + <A attr:class="example" href="/naive-fallback"> + "Leptos " + <code>"<Script>"</code> + <small>"... with fallback"</small> + </A> + <A attr:class="example" href="/signal-effect-script"> + "Leptos Signal + Effect" + <small>"an idiomatic Leptos solution"</small> + </A> + <A attr:class="subexample section" href="/custom-event"> + "Hydrated Event" + <small>"using "<code>"js_sys"</code>"/"<code>"web_sys"</code></small> + </A> + <A attr:class="example" href="/wasm-bindgen-naive"> + "Using " + <code>"wasm-bindgen"</code> + <small>"naively to start with"</small> + </A> + <A attr:class="example" href="/wasm-bindgen-event"> + "Using " + <code>"wasm-bindgen"</code> + <small>"overcomplication with events"</small> + </A> + <A attr:class="example" href="/wasm-bindgen-effect"> + "Using " + <code>"wasm-bindgen"</code> + <small>"lazily delay DOM manipulation"</small> + </A> + <A attr:class="example" href="/wasm-bindgen-direct"> + "Using " + <code>"wasm-bindgen"</code> + <small>"without DOM manipulation"</small> + </A> <A attr:class="example section" href="/wasm-bindgen-direct-fixed"> - "Using "<code>"wasm-bindgen"</code> + "Using " + <code>"wasm-bindgen"</code> <small>"corrected with signal + effect"</small> </A> - <a id="reset" href="/" target="_self">"Restart/Rehydrate" - <small>"to make things work again"</small></a> + <a id="reset" href="/" target="_self"> + "Restart/Rehydrate" + <small>"to make things work again"</small> + </a> </nav> <main> <div id="notice"> @@ -77,20 +110,54 @@ pub fn App() -> impl IntoView { <article> <h1>"Leptos JavaScript Integration Demo with SSR in Axum"</h1> <FlatRoutes fallback> - <Route path=path!("") view=HomePage/> - <Route path=path!("naive") view=Naive ssr=SsrMode::Async/> - <Route path=path!("naive-alt") view=|| view! { <NaiveEvent/> } ssr=SsrMode::Async/> - <Route path=path!("naive-hook") view=|| view! { <NaiveEvent hook=true/> } ssr=SsrMode::Async/> - <Route path=path!("naive-fallback") view=|| view! { - <NaiveEvent hook=true fallback=true/> - } ssr=SsrMode::Async/> - <Route path=path!("signal-effect-script") view=CodeDemoSignalEffect ssr=SsrMode::Async/> - <Route path=path!("custom-event") view=CustomEvent ssr=SsrMode::Async/> - <Route path=path!("wasm-bindgen-naive") view=WasmBindgenNaive ssr=SsrMode::Async/> - <Route path=path!("wasm-bindgen-event") view=WasmBindgenJSHookReadyEvent ssr=SsrMode::Async/> - <Route path=path!("wasm-bindgen-effect") view=WasmBindgenEffect ssr=SsrMode::Async/> - <Route path=path!("wasm-bindgen-direct") view=WasmBindgenDirect ssr=SsrMode::Async/> - <Route path=path!("wasm-bindgen-direct-fixed") view=WasmBindgenDirectFixed ssr=SsrMode::Async/> + <Route path=path!("") view=HomePage /> + <Route path=path!("naive") view=Naive ssr=SsrMode::Async /> + <Route + path=path!("naive-alt") + view=|| view! { <NaiveEvent /> } + ssr=SsrMode::Async + /> + <Route + path=path!("naive-hook") + view=|| view! { <NaiveEvent hook=true /> } + ssr=SsrMode::Async + /> + <Route + path=path!("naive-fallback") + view=|| view! { <NaiveEvent hook=true fallback=true /> } + ssr=SsrMode::Async + /> + <Route + path=path!("signal-effect-script") + view=CodeDemoSignalEffect + ssr=SsrMode::Async + /> + <Route path=path!("custom-event") view=CustomEvent ssr=SsrMode::Async /> + <Route + path=path!("wasm-bindgen-naive") + view=WasmBindgenNaive + ssr=SsrMode::Async + /> + <Route + path=path!("wasm-bindgen-event") + view=WasmBindgenJSHookReadyEvent + ssr=SsrMode::Async + /> + <Route + path=path!("wasm-bindgen-effect") + view=WasmBindgenEffect + ssr=SsrMode::Async + /> + <Route + path=path!("wasm-bindgen-direct") + view=WasmBindgenDirect + ssr=SsrMode::Async + /> + <Route + path=path!("wasm-bindgen-direct-fixed") + view=WasmBindgenDirectFixed + ssr=SsrMode::Async + /> </FlatRoutes> </article> </main> @@ -101,28 +168,37 @@ pub fn App() -> impl IntoView { #[component] fn HomePage() -> impl IntoView { view! { - <p>" - This example application demonstrates a number of ways that JavaScript may be included and used - with Leptos naively, describing and showing the shortcomings and failures associated with each of - them for both SSR (Server-Side Rendering) and CSR (Client-Side Rendering) with hydration, before - leading up to the idiomatic solutions where they work as expected. - "</p> - <p>" - For the demonstrations, "<a href="https://github.com/highlightjs/highlight.js"><code> - "highlight.js"</code></a>" will be invoked from within this Leptos application by the examples + <p> + " + This example application demonstrates a number of ways that JavaScript may be included and used + with Leptos naively, describing and showing the shortcomings and failures associated with each of + them for both SSR (Server-Side Rendering) and CSR (Client-Side Rendering) with hydration, before + leading up to the idiomatic solutions where they work as expected. + " + </p> + <p> + " + For the demonstrations, "<a href="https://github.com/highlightjs/highlight.js"> + <code>"highlight.js"</code> + </a>" will be invoked from within this Leptos application by the examples linked on the side bar. Since the library to be integrated is a JavaScript library, it must be enabled to fully appreciate this demo, and having the browser's developer tools/console opened is recommended as the logs will indicate the effects and issues as they happen. - "</p> - <p>" - Examples 1 to 5 are primarily JavaScript based, where the integration code is included as "<code> - "<script>"</code>" tags, with example 5 (final example of the group) being the idiomatic solution + " + </p> + <p> + " + Examples 1 to 5 are primarily JavaScript based, where the integration code is included as " + <code>"<script>"</code> + " tags, with example 5 (final example of the group) being the idiomatic solution that runs without errors or panic during hydration, plus an additional example 5.1 showing how to get hydration to dispatch an event for JavaScript libraries should that be required. Examples 6 - to 10 uses "<code>"wasm-bindgen"</code>" to call out to the JavaScript library from Rust, starting + to 10 uses "<code>"wasm-bindgen"</code> + " to call out to the JavaScript library from Rust, starting off with naive examples that mimics JavaScript conventions, again with the final example of the group (example 10) being the fully working version that embraces the use of Rust. - "</p> + " + </p> } } @@ -138,18 +214,20 @@ fn CodeDemo() -> impl IntoView { Suspend::new(async move { let hook = use_context::<CodeDemoHook>().map(|h| { leptos::logging::log!("use context suspend JS"); - view! { - <Script>{h.js_hook}</Script> - } + view! { <Script>{h.js_hook}</Script> } }); view! { - <pre><code class="language-rust">{code.await}</code></pre> + <pre> + <code class="language-rust">{code.await}</code> + </pre> {hook} } }) }; view! { - <p>"Explanation on what is being demonstrated follows after the following code example table."</p> + <p> + "Explanation on what is being demonstrated follows after the following code example table." + </p> <div id="code-demo"> <table> <thead> @@ -160,11 +238,15 @@ fn CodeDemo() -> impl IntoView { </thead> <tbody> <tr> - <td><pre><code class="language-rust">{CH03_05A}</code></pre></td> <td> - <Suspense fallback=move || view! { <p>"Loading code example..."</p> }> - {code_view} - </Suspense> + <pre> + <code class="language-rust">{CH03_05A}</code> + </pre> + </td> + <td> + <Suspense fallback=move || { + view! { <p>"Loading code example..."</p> } + }>{code_view}</Suspense> </td> </tr> </tbody> @@ -179,78 +261,111 @@ fn Naive() -> impl IntoView { <script>hljs.highlightAll();</script>"#; view! { <h2>"Showing what happens when script inclusion is done naively"</h2> - <CodeDemo/> - <p>" - This page demonstrates what happens (or doesn't happen) when it is assumed that the "<code> - "highlight.js"</code>" library can just be included from some CDN (well, hosted locally for this + <CodeDemo /> + <p> + " + This page demonstrates what happens (or doesn't happen) when it is assumed that the " + <code>"highlight.js"</code> + " library can just be included from some CDN (well, hosted locally for this example) as per their instructions for basic usage in the browser, specifically: - "</p> - <div><pre><code class="language-html">{loader}</code></pre></div> - <p>" - The following actions should be taken in order to fully experience the things that do not work as - expected: - "</p> + " + </p> + <div> + <pre> + <code class="language-html">{loader}</code> + </pre> + </div> + <p> + " + The following actions should be taken in order to fully experience the things that do not work as + expected: + " + </p> <ol> - <li>" + <li> + " You may find that during the initial load of this page when first navigating to here from \"Introduction\" (do navigate there, reload to reinitiate this application to properly replicate the behavior, or simply use the Restart link at the bottom), none of the code examples below are highlighted. - "</li> - <li>" + " + </li> + <li> + " Go back and then forward again using the browser's navigation system the inline code block - will become highlighted. The cause is due to "<code>"highlight.js"</code>" being loaded in a - standard "<code>"<script>"</code>" tag that is part of this component and initially it wasn't - loaded before the call to "<code>"hljs.highlightAll();"</code>" was made. Later, when the + will become highlighted. The cause is due to "<code>"highlight.js"</code> + " being loaded in a + standard "<code>"<script>"</code> + " tag that is part of this component and initially it wasn't + loaded before the call to "<code>"hljs.highlightAll();"</code> + " was made. Later, when the component gets re-rendered the second time, the code is finally available to ensure one of them works (while also reloading the script, which probably isn't desirable for this use case). - "</li> - <li>" - If you have the browser reload this page, you will find that "<strong>"both"</strong>" code + " + </li> + <li> + " + If you have the browser reload this page, you will find that " + <strong>"both"</strong>" code examples now appear to highlight correctly, yay! However you will also find that the browser's back button appears to do nothing at all (even though the address bar may have changed), and that most of the links on the side-bar are non-functional. A message should have popped up at the top indicating that the application has panicked. - "<details>" + " + <details> + " "<summary>"Details about the cause of the crash:"</summary> - <p>" - The cause here is because the hydration system found a node where text was expected, a - simple violation of the application's invariant. Specifically, the code block - originally contained plain text, but with highlighting that got changed to some HTML - markup "<em>"before"</em>" hydration happened, completely ouside of expectations. + <p> + " + The cause here is because the hydration system found a node where text was expected, a + simple violation of the application's invariant. Specifically, the code block + originally contained plain text, but with highlighting that got changed to some HTML + markup "<em>"before"</em> + " hydration happened, completely ouside of expectations. Generally speaking, a panic is the worst kind of error, as it is a hard crash which stops the application from working, and in this case the reactive system is in a completely non-functional state. - "</p> - <p>" - Fortunately for this application, some internal links within this application have - been specifically excluded from the reactive system (specifically the restart links, - so they remain usable as they are just standard links which include the bottommost one - of the side bar and the one that should become visible as a notification as the panic - happened at the top - both may be used to navigate non-reactively back to the - homepage. - "</p> - <p>" - Navigating back after using the non-reactive links will also restart the application, - so using that immediately after to return to this page will once again trigger the - same condition that will result the hydration to panic. If you wish to maintain the - push state within the history, simply use the browser navigation to navigate through - those pushed addresses and find one that may be reloaded without causing the crash, - and then go the opposite direction the same number of steps to get back to here. - "</p>" - "</details>" - "</li> - <li>" + " + </p> + <p> + " + Fortunately for this application, some internal links within this application have + been specifically excluded from the reactive system (specifically the restart links, + so they remain usable as they are just standard links which include the bottommost one + of the side bar and the one that should become visible as a notification as the panic + happened at the top - both may be used to navigate non-reactively back to the + homepage. + " + </p> + <p> + " + Navigating back after using the non-reactive links will also restart the application, + so using that immediately after to return to this page will once again trigger the + same condition that will result the hydration to panic. If you wish to maintain the + push state within the history, simply use the browser navigation to navigate through + those pushed addresses and find one that may be reloaded without causing the crash, + and then go the opposite direction the same number of steps to get back to here. + " + </p>" + " + </details>" + " + </li> + <li> + " In the working CSR state, if you continue to use the browser's navigation system to go back to home and forward back to this page, you will find that the the browser's console log is spammed with the different delays added to the loading of the standard highlight.js file. The - cause is because the script is unloaded/reloaded every time its "<code>"<script>"</code>" tag + cause is because the script is unloaded/reloaded every time its " + <code>"<script>"</code>" tag is re-created by this component. This may or may not be a desirable behavior, so where exactly these tags are situated will matter - if the goal is to load the script once, the tag should be provided above the Router. - "</li> - <li>" + " + </li> + <li> + " Simply use the restart links to get back home and move onto the next example - or come back here, if you wish - while all the examples can be used out of order, the intended broken behaviors being demonstrated are best experienced by going home using the reactive link at the @@ -258,7 +373,8 @@ fn Naive() -> impl IntoView { subtly broken behavior(s) in arbitrary order can and will amplify into further unexpected and potentially hard to reproduce behaviors. What they are and why they happen are left as exercise for the users and readers of this demo application. - "</li> + " + </li> </ol> <script src="/highlight.min.js"></script> <script>"hljs.highlightAll();"</script> @@ -288,65 +404,95 @@ if (window.hljs) { if fallback { view! { <ol> - <li>" - In this iteration, the following load hook is set in a "<code>"<Script>"</code>" - component after the dynamically loaded code example." - <pre><code class="language-javascript">{js_hook}</code></pre> + <li> + " + In this iteration, the following load hook is set in a " + <code>"<Script>"</code>" + component after the dynamically loaded code example." <pre> + <code class="language-javascript">{js_hook}</code> + </pre> </li> - <li><strong>CSR</strong>" + <li> + <strong>CSR</strong> + " This works much better now under CSR due to the fallback that checks whether the library is already loaded or not. Using the library directly if it's already loaded and only register the event otherwise solves the rendering issue under CSR. - "</li> - <li><strong>SSR</strong>" + " + </li> + <li> + <strong>SSR</strong> + " Much like the second example, hydration will still panic some of the time as per the race condition that was described. - "</li> + " + </li> </ol> - <p>" - All that being said, all these naive examples still result in hydration being - non-functional in varying degrees of (non-)reproducibility due to race conditions. Is - there any way to fix this? Is "<code>"wasm-bindgen"</code>" the only answer? What if the + <p> + " + All that being said, all these naive examples still result in hydration being + non-functional in varying degrees of (non-)reproducibility due to race conditions. Is + there any way to fix this? Is "<code>"wasm-bindgen"</code> + " the only answer? What if the goal is to incorporate external scripts that change often and thus can't easily have bindings built? Follow onto the next examples to solve some of this, at the very least prevent the panic during hydration. - "</p> - + " + </p> }.into_any() } else { view! { <ol> - <li>" - In this iteration, the following load hook is set in a "<code>"<Script>"</code>" - component after the dynamically loaded code example." - <pre><code class="language-javascript">{js_hook}</code></pre> + <li> + " + In this iteration, the following load hook is set in a " + <code>"<Script>"</code>" + component after the dynamically loaded code example." <pre> + <code class="language-javascript">{js_hook}</code> + </pre> </li> - <li><strong>CSR</strong>" + <li> + <strong>CSR</strong> + " Unfortunately, this still doesn't work reliably to highlight both code examples, in fact, none of the code examples may highlight at all! Placing the JavaScript loader - hook inside a "<code>Suspend</code>" will significantly increase the likelihood that + hook inside a " + <code>Suspend</code> + " will significantly increase the likelihood that the event will be fired long before the loader adds the event hook. As a matter of fact, the highlighting is likely to only work with the largest latencies added for - the loading of "<code>"highlight.js"</code>", but at least both code examples will + the loading of " + <code>"highlight.js"</code> + ", but at least both code examples will highlight when working. - "</li> - <li><strong>SSR</strong>" + " + </li> + <li> + <strong>SSR</strong> + " Much like the second example, hydration will still panic some of the time as per the race condition that was described - basically if the timing results in CSR not showing highlight code, the code will highlight here in SSR but will panic during hydration. - "</li> + " + </li> </ol> }.into_any() } } else { view! { <ol> - <li>" - In this iteration, the following hook is set in a "<code>"<Script>"</code>" component + <li> + " + In this iteration, the following hook is set in a "<code>"<Script>"</code> + " component immediately following the one that loaded "<code>"highlight.js"</code>". - "<pre><code class="language-javascript">{js_hook}</code></pre> + "<pre> + <code class="language-javascript">{js_hook}</code> + </pre> </li> - <li><strong>CSR</strong>" + <li> + <strong>CSR</strong> + " Unfortunately, the hook is being set directly on this component, rather than inside the view for the dynamic block. Given the nature of asynchronous loading which results in the uncertainty of the order of events, it may or may not result in the dynamic code block (or @@ -356,37 +502,50 @@ if (window.hljs) { console and it may be referred to witness its effects on what it does under CSR - look for the line that might say \"loaded standard highlight.js with a minimum latency of 40 ms\". Test this by going from home to here and then navigating between them using the browser's - back and forward feature for convenience - do ensure the "<code>"highlight.js" </code>" + back and forward feature for convenience - do ensure the " + <code>"highlight.js"</code> + " isn't being cached by the browser. - "</li> - <li><strong>SSR</strong>" + " + </li> + <li> + <strong>SSR</strong> + " Moreover, hydration will panic if the highlight script is loaded before hydration is completed (from the resulting DOM mismatch after code highlighting). Refreshing here - repeatedly may trigger the panic only some of the time when the "<code>"highlight.js" - </code>" script is loaded under the lowest amounts of artificial delay, as even under no + repeatedly may trigger the panic only some of the time when the " + <code>"highlight.js"</code> + " script is loaded under the lowest amounts of artificial delay, as even under no latency the hydration can still succeed due to the non-deterministic nature of this race condition. - "</li> + " + </li> </ol> }.into_any() }; // FIXME Seems like <Script> require a text node, otherwise hydration error from marker mismatch view! { <h2>"Using the Leptos "<code>"<Script>"</code>" component asynchronously instead"</h2> - <CodeDemo/> - <Script id="hljs-src" async_="true" src="/highlight.min.js">""</Script> + <CodeDemo /> + <Script id="hljs-src" async_="true" src="/highlight.min.js"> + "" + </Script> // Example 2's <Script> invocation; Example 3 and 4 will be provided via a context to allow the // inclusion of the `highlightAll()` call in the Suspend - {(!hook).then(|| view! { <Script>{render_hook}</Script>})} - <p>" - What the "<code>"<Script>"</code>" component does is to ensure the "<code>"<script>"</code>" tag + {(!hook).then(|| view! { <Script>{render_hook}</Script> })} + <p> + " + What the "<code>"<Script>"</code>" component does is to ensure the " + <code>"<script>"</code>" tag is placed in the document head in the order it is defined in a given component, rather than at where it was placed into the DOM. Note that it is also a reactive component, much like the first example, it gets unloaded under CSR when the component is no longer active, In this improved - version, "<code>"highlight.js"</code>" is also loaded asynchronously (using the "<code>"async" - </code>" attribute), to allow an event listener that can delay highlighting to after the library + version, "<code>"highlight.js"</code>" is also loaded asynchronously (using the " + <code>"async"</code> + " attribute), to allow an event listener that can delay highlighting to after the library is loaded. This should all work out fine, right? - "</p> + " + </p> {explanation} } } @@ -420,20 +579,31 @@ Promise.all(events).then(() => {{ // FIXME Seems like <Script> require a text node, otherwise hydration error from marker mismatch view! { <h2>"Have Leptos dispatch an event when body is hydrated"</h2> - <CodeDemo/> - <Script id="hljs-src" async_="true" src="/highlight.min.js">""</Script> - <p>" - So if using events fixes problems with timing issues, couldn't Leptos provide an event to signal - that the body is hydrated? Well, this problem is typically solved by having a signal in the - component, and then inside the "<code>"Suspend"</code>" provide an "<code>"Effect"</code>" that - would set the signal to "<code>"Some"</code>" string that will then mount the "<code>"<Script>" - </code>" onto the body. However, if a hydrated event is desired from within JavaScript (e.g. + <CodeDemo /> + <Script id="hljs-src" async_="true" src="/highlight.min.js"> + "" + </Script> + <p> + " + So if using events fixes problems with timing issues, couldn't Leptos provide an event to signal + that the body is hydrated? Well, this problem is typically solved by having a signal in the + component, and then inside the "<code>"Suspend"</code>" provide an " + <code>"Effect"</code>" that + would set the signal to "<code>"Some"</code>" string that will then mount the " + <code>"<Script>"</code> + " onto the body. However, if a hydrated event is desired from within JavaScript (e.g. where some existing JavaScript library/framework is managing event listeners for some particular - reason), given that typical Leptos applications provide the "<code>"fn hydate()"</code>" (usually - in "<code>" lib.rs"</code>"), that can be achieved by providing the following after "<code> - "leptos::mount::hydrate_body(App);"</code>". - "</p> - <div><pre><code class="language-rust">{format!(r#"#[cfg(feature = "hydrate")] + reason), given that typical Leptos applications provide the "<code>"fn hydate()"</code> + " (usually + in "<code>" lib.rs"</code>"), that can be achieved by providing the following after " + <code>"leptos::mount::hydrate_body(App);"</code>". + " + </p> + <div> + <pre> + <code class="language-rust"> + {format!( + r#"#[cfg(feature = "hydrate")] #[wasm_bindgen::prelude::wasm_bindgen] pub fn hydrate() {{ use app::App; @@ -458,29 +628,47 @@ pub fn hydrate() {{ let document = leptos::prelude::document(); document.dispatch_event(&event) .expect("error dispatching hydrated event"); -}}"# - )}</code></pre></div> - <p>" - With the notification that hydration is completed, the following JavaScript code may be called - inside "<code>"Suspense"</code>" block (in this live example, it's triggered by providing the - following JavaScript code via a "<code>"provide_context"</code>" which the code rendering +}}"#, + )} + </code> + </pre> + </div> + <p> + " + With the notification that hydration is completed, the following JavaScript code may be called + inside "<code>"Suspense"</code> + " block (in this live example, it's triggered by providing the + following JavaScript code via a "<code>"provide_context"</code> + " which the code rendering component will then use within a "<code>"Suspend"</code>"): - "</p> - <div><pre><code class="language-javascript">{js_hook}</code></pre></div> - <p>" - For this simple example with a single "<code>"Suspense"</code>", no matter what latency there is, - in whichever order the API calls are completed, the setup ensures that "<code>"highlightAll()" - </code>" is called only after hydration is done and also after the delayed content is properly + " + </p> + <div> + <pre> + <code class="language-javascript">{js_hook}</code> + </pre> + </div> + <p> + " + For this simple example with a single "<code>"Suspense"</code> + ", no matter what latency there is, + in whichever order the API calls are completed, the setup ensures that " + <code>"highlightAll()"</code> + " is called only after hydration is done and also after the delayed content is properly rendered onto the DOM. Specifically, only use the event to wait for the required resource if it is not set to a ready state, and wait for all the events to become ready before actually calling the function. - "</p> - <p>" - If there are multiple "<code>"Suspense"</code>", it will be a matter of adding all the event - listeners that will respond to the completion of all the "<code>"Suspend"</code>"ed futures, which + " + </p> + <p> + " + If there are multiple "<code>"Suspense"</code> + ", it will be a matter of adding all the event + listeners that will respond to the completion of all the "<code>"Suspend"</code> + "ed futures, which will then invoke the code highlighting function. - "</p> - // Leaving this last bit as a bonus page? As an exercise for the readers? + " + </p> } } @@ -509,19 +697,26 @@ if (window.hljs) { set_script.set(Some(render_call.to_string())); }); view! { - <pre><code class="language-rust">{code.await}</code></pre> - { - move || script.get().map(|script| { - view! { <Script>{script}</Script> } - }) - } + <pre> + <code class="language-rust">{code.await}</code> + </pre> + <ShowLet some=script let:script> + <Script>{script}</Script> + </ShowLet> } }) }; view! { - <Script id="hljs-src" async_="true" src="/highlight.min.js">""</Script> - <h2>"Using signal + effect to dynamically set "<code>"<Script>"</code>" tag as view is mounted"</h2> - <p>"Explanation on what is being demonstrated follows after the following code example table."</p> + <Script id="hljs-src" async_="true" src="/highlight.min.js"> + "" + </Script> + <h2> + "Using signal + effect to dynamically set "<code>"<Script>"</code> + " tag as view is mounted" + </h2> + <p> + "Explanation on what is being demonstrated follows after the following code example table." + </p> <div id="code-demo"> <table> <thead> @@ -532,72 +727,97 @@ if (window.hljs) { </thead> <tbody> <tr> - <td><pre><code class="language-rust">{CH03_05A}</code></pre></td> <td> - <Suspense fallback=move || view! { <p>"Loading code example..."</p> }> - {code_view} - </Suspense> + <pre> + <code class="language-rust">{CH03_05A}</code> + </pre> + </td> + <td> + <Suspense fallback=move || { + view! { <p>"Loading code example..."</p> } + }>{code_view}</Suspense> </td> </tr> </tbody> </table> </div> - <p>" - To properly ensure the "<code>"<Script>"</code>" tag containing the initialization code for the - target JavaScript usage is executed after the "<code>"Suspend"</code>"ed view is fully rendered + <p> + " + To properly ensure the "<code>"<Script>"</code> + " tag containing the initialization code for the + target JavaScript usage is executed after the "<code>"Suspend"</code> + "ed view is fully rendered and mounted onto the DOM, with the use of an effect that sets a signal to trigger the rendering inside the suspend will achieve exactly that. That was a mouthful, so let's look at the code for that then: - "</p> - <div><pre><code class="language-rust">r##"#[component] -fn CodeDemoSignalEffect() -> impl IntoView { - let render_call = r#" -if (window.hljs) { - hljs.highlightAll(); -} else { - document.querySelector('#hljs-src') - .addEventListener('load', (e) => { hljs.highlightAll() }, false); -};"#; - let code = Resource::new(|| (), |_| fetch_code()); - let (script, set_script) = signal(None::<String>); - let code_view = move || { - Suspend::new(async move { - Effect::new(move |_| { - set_script.set(Some(render_call.to_string())); - }); - view! { - <pre><code class="language-rust">{code.await}</code></pre> - { + " + </p> + <div> + <pre> + <code class="language-rust"> + r##"#[component] + fn CodeDemoSignalEffect() -> impl IntoView { + let render_call = r#" + if (window.hljs) { + hljs.highlightAll(); + } else { + document.querySelector('#hljs-src') + .addEventListener('load', (e) => { hljs.highlightAll() }, false); + };"#; + let code = Resource::new(|| (), |_| fetch_code()); + let (script, set_script) = signal(None::<String>); + let code_view = move || { + Suspend::new(async move { + Effect::new(move |_| { + set_script.set(Some(render_call.to_string())); + }); + view! { + <pre><code class="language-rust">{code.await}</code></pre> + { move || script.get().map(|script| { - view! { <Script>{script}</Script> } + view! { <Script>{script}</Script> } }) - } - } - }) - }; - view! { - <Script id="hljs-src" async_="true" src="/highlight.min.js">""</Script> - <Suspense fallback=move || view! { <p>"Loading code example..."</p> }> - {code_view} - </Suspense> - } -}"##</code></pre></div> - <p>" - The "<code>"Suspend"</code>" ensures the asynchronous "<code>"Resource"</code>" will be completed + } + } + }) + }; + view! { + <Script id="hljs-src" async_="true" src="/highlight.min.js">""</Script> + <Suspense fallback=move || view! { <p>"Loading code example..."</p> }> + {code_view} + </Suspense> + } + }"## + </code> + </pre> + </div> + <p> + " + The "<code>"Suspend"</code>" ensures the asynchronous "<code>"Resource"</code> + " will be completed before the view is returned, which will be mounted onto the DOM, but the initial value of the - signal "<code>"script"</code>" will be "<code>"None"</code>", so no "<code>"<Script>"</code>" tag - will be rendered at that stage. Only after the suspended view is mounted onto the DOM the "<code> - "Effect"</code>" will run, which will call "<code>"set_script"</code>" with "<code>"Some"</code>" - value which will finally populate the "<code>"<Script>"</code>" tag with the desired JavaScript to + signal "<code>"script"</code>" will be "<code>"None"</code>", so no " + <code>"<Script>"</code>" tag + will be rendered at that stage. Only after the suspended view is mounted onto the DOM the " + <code>"Effect"</code>" will run, which will call "<code>"set_script"</code>" with " + <code>"Some"</code>" + value which will finally populate the "<code>"<Script>"</code> + " tag with the desired JavaScript to be executed, in this case invoke the code highlighting feature if available otherwise wait for it. - "</p> - <p>" - If there are multiple "<code>"Suspense"</code>", it will be a matter of adding the event to be - dispatched to "<code>"set_script.set"</code>" so that it gets dispatched for the component, and + " + </p> + <p> + " + If there are multiple "<code>"Suspense"</code> + ", it will be a matter of adding the event to be + dispatched to "<code>"set_script.set"</code> + " so that it gets dispatched for the component, and then elsewhere above all those components a JavaScript list will tracking all the events will be - waited on by "<code>"Promise.all"</code>", where its completion will finally invoke the desired + waited on by "<code>"Promise.all"</code> + ", where its completion will finally invoke the desired JavaScript function. - "</p> + " + </p> } } @@ -612,109 +832,115 @@ fn CodeDemoWasm(mode: WasmDemo) -> impl IntoView { let code = Resource::new(|| (), |_| fetch_code()); let suspense_choice = match mode { WasmDemo::Naive => view! { - <Suspense fallback=move || view! { <p>"Loading code example..."</p> }>{ - move || Suspend::new(async move { + <Suspense fallback=move || { + view! { <p>"Loading code example..."</p> } + }> + {move || Suspend::new(async move { view! { - <pre><code class="language-rust">{code.await}</code></pre> + <pre> + <code class="language-rust">{code.await}</code> + </pre> + {#[cfg(not(feature = "ssr"))] { - #[cfg(not(feature = "ssr"))] - { - use crate::hljs::highlight_all; - leptos::logging::log!("calling highlight_all"); - highlight_all(); - } - } + use crate::hljs::highlight_all; + leptos::logging::log!("calling highlight_all"); + highlight_all(); + }} } - }) - }</Suspense> + })} + </Suspense> }.into_any(), WasmDemo::ReadyEvent => view! { - <Suspense fallback=move || view! { <p>"Loading code example..."</p> }>{ - move || Suspend::new(async move { + <Suspense fallback=move || { + view! { <p>"Loading code example..."</p> } + }> + {move || Suspend::new(async move { view! { - <pre><code class="language-rust">{code.await}</code></pre> + <pre> + <code class="language-rust">{code.await}</code> + </pre> + {#[cfg(not(feature = "ssr"))] { - #[cfg(not(feature = "ssr"))] - { - use crate::hljs; - use wasm_bindgen::{closure::Closure, JsCast}; - - let document = document(); - // Rules relating to hydration still applies when loading via SSR! Changing - // the dom before hydration is done is still problematic, as the same issues - // such as the panic as demonstrated in the relevant JavaScript demo. - let hydrate_listener = Closure::<dyn Fn(_)>::new(move |_: web_sys::Event| { + use crate::hljs; + use wasm_bindgen::{closure::Closure, JsCast}; + let document = document(); + let hydrate_listener = Closure::< + dyn Fn(_), + >::new(move |_: web_sys::Event| { leptos::logging::log!("wasm hydration_listener highlighting"); hljs::highlight_all(); - }).into_js_value(); - document.add_event_listener_with_callback( + }) + .into_js_value(); + document + .add_event_listener_with_callback( LEPTOS_HYDRATED, hydrate_listener.as_ref().unchecked_ref(), - ).expect("failed to add event listener to document"); - - // For CSR rendering, wait for the hljs_hook which will be fired when this - // suspended bit is fully mounted onto the DOM, and this is done using a - // JavaScript shim described below. - let csr_listener = Closure::<dyn FnMut(_)>::new(move |_: web_sys::Event| { + ) + .expect("failed to add event listener to document"); + let csr_listener = Closure::< + dyn FnMut(_), + >::new(move |_: web_sys::Event| { leptos::logging::log!("wasm csr_listener highlighting"); hljs::highlight_all(); - }).into_js_value(); - let options = web_sys::AddEventListenerOptions::new(); - options.set_once(true); - // FIXME this actually is not added as a unique function so after a quick re- - // render will re-add this as a new listener, which causes a double call - // to highlightAll. To fix this there needs to be a way to put the listener - // and keep it unique, but this looks to be rather annoying to do from within - // this example... - document.add_event_listener_with_callback_and_add_event_listener_options( + }) + .into_js_value(); + let options = web_sys::AddEventListenerOptions::new(); + options.set_once(true); + document + .add_event_listener_with_callback_and_add_event_listener_options( "hljs_hook", csr_listener.as_ref().unchecked_ref(), &options, - ).expect("failed to add event listener to document"); - leptos::logging::log!("wasm csr_listener listener added"); - - // Dispatch the event when this view is finally mounted onto the DOM. - request_animation_frame(move || { - let event = web_sys::Event::new("hljs_hook") - .expect("error creating hljs_hook event"); - document.dispatch_event(&event) - .expect("error dispatching hydrated event"); - }); - // Alternative, use a script tag, but at that point, you might as well write - // all of the above in JavaScript because in this simple example none of the - // above is native to Rust or Leptos. - } - } + ) + .expect("failed to add event listener to document"); + leptos::logging::log!("wasm csr_listener listener added"); + request_animation_frame(move || { + let event = web_sys::Event::new("hljs_hook") + .expect("error creating hljs_hook event"); + document + .dispatch_event(&event) + .expect("error dispatching hydrated event"); + }); + }} } - }) - }</Suspense> + })} + </Suspense> }.into_any(), WasmDemo::RequestAnimationFrame => view! { - <Suspense fallback=move || view! { <p>"Loading code example..."</p> }>{ - move || Suspend::new(async move { + <Suspense fallback=move || { + view! { <p>"Loading code example..."</p> } + }> + {move || Suspend::new(async move { Effect::new(move |_| { request_animation_frame(move || { - leptos::logging::log!("request_animation_frame invoking hljs::highlight_all"); - // under SSR this is an noop, but it wouldn't be called under there anyway because - // it isn't the isomorphic version, i.e. Effect::new_isomorphic(...). + leptos::logging::log!( + "request_animation_frame invoking hljs::highlight_all" + ); crate::hljs::highlight_all(); }); }); + // under SSR this is an noop, but it wouldn't be called under there anyway because + // it isn't the isomorphic version, i.e. Effect::new_isomorphic(...). view! { - <pre><code class="language-rust">{code.await}</code></pre> + <pre> + <code class="language-rust">{code.await}</code> + </pre> } - }) - }</Suspense> + })} + </Suspense> }.into_any(), }; view! { - <p>" - The syntax highlighting shown in the table below is done by invoking "<code>"hljs.highlightAll()" - </code>" via the binding generated using "<code>"wasm-bindgen"</code>" - thus the ES version of " - <code>"highlight.js"</code>" is loaded by the output bundle generated by Leptos under this set of + <p> + " + The syntax highlighting shown in the table below is done by invoking " + <code>"hljs.highlightAll()"</code>" via the binding generated using " + <code>"wasm-bindgen"</code>" - thus the ES version of " <code>"highlight.js"</code> + " is loaded by the output bundle generated by Leptos under this set of demonstrations. However, things may still not work as expected, with the explanation on what is being demonstrated follows after the following code example table. - "</p> + " + </p> <div id="code-demo"> <table> <thead> @@ -725,7 +951,11 @@ fn CodeDemoWasm(mode: WasmDemo) -> impl IntoView { </thead> <tbody> <tr> - <td><pre><code class="language-rust">{CH03_05A}</code></pre></td> + <td> + <pre> + <code class="language-rust">{CH03_05A}</code> + </pre> + </td> <td>{suspense_choice}</td> </tr> </tbody> @@ -753,27 +983,40 @@ fn WasmBindgenNaive() -> impl IntoView { }</Suspense>"#; view! { <h2>"Will "<code>"wasm-bindgen"</code>" magically avoid all the problems?"</h2> - <CodeDemoWasm mode=WasmDemo::Naive/> - <p>" - Well, the naively done example clearly does not work, as the behavior of this demo is almost - exactly like the very first naive JavaScript example (after the script loaded), where only the - inline code block will highlight under CSR and hydration is broken when trying to load this under - SSR. This is the consequence of porting the logic naively. In this example, the calling of - "<code>"hljs::highlight_all()"</code>" is located inside a "<code>"Suspend"</code>" immediately + <CodeDemoWasm mode=WasmDemo::Naive /> + <p> + " + Well, the naively done example clearly does not work, as the behavior of this demo is almost + exactly like the very first naive JavaScript example (after the script loaded), where only the + inline code block will highlight under CSR and hydration is broken when trying to load this under + SSR. This is the consequence of porting the logic naively. In this example, the calling of + "<code>"hljs::highlight_all()"</code>" is located inside a "<code>"Suspend"</code> + " immediately after the code block, but it doesn't mean the execution will apply to that because it hasn't been mounted onto the DOM itself for "<code>"highlight.js"</code>" to process. - "</p> - <p>" - Similarly, SSR may also error under a similar mechanism, which again breaks hydration because the - code is run on the dehydrated nodes before hydration has happened. Using event listeners via - "<code>"web_sys"</code>" in a similar manner like the JavaScript based solutions shown previously + " + </p> + <p> + " + Similarly, SSR may also error under a similar mechanism, which again breaks hydration because the + code is run on the dehydrated nodes before hydration has happened. Using event listeners via + "<code>"web_sys"</code> + " in a similar manner like the JavaScript based solutions shown previously can fix this, but there are other approaches also. - "</p> - <p>" - For a quick reference, the following is the "<code>"Suspense"</code>" that would ultimately render + " + </p> + <p> + " + For a quick reference, the following is the "<code>"Suspense"</code> + " that would ultimately render the dynamic code block: - "</p> - <div><pre><code class="language-rust">{example}</code></pre></div> + " + </p> + <div> + <pre> + <code class="language-rust">{example}</code> + </pre> + </div> } } @@ -832,36 +1075,48 @@ fn WasmBindgenJSHookReadyEvent() -> impl IntoView { view! { <h2>"Using "<code>"wasm-bindgen"</code>" with proper consideration"</h2> - <CodeDemoWasm mode=WasmDemo::ReadyEvent/> - <p>" - Well, this works a lot better, under SSR the code is highlighted only after hydration to avoid the - panic, and under CSR a new event is created for listening and responding to for the rendering to - happen only after the suspended node is populated onto the DOM. There is a bit of a kink with the - way this is implemented, but it largely works. - "</p> - <p>" - The code that drives this is needlessly overcomplicated, to say the least. This is what got added - to the "<code>"view! {...}"</code>" from the last example: - "</p> + <CodeDemoWasm mode=WasmDemo::ReadyEvent /> + <p> + " + Well, this works a lot better, under SSR the code is highlighted only after hydration to avoid the + panic, and under CSR a new event is created for listening and responding to for the rendering to + happen only after the suspended node is populated onto the DOM. There is a bit of a kink with the + way this is implemented, but it largely works. + " + </p> + <p> + " + The code that drives this is needlessly overcomplicated, to say the least. This is what got added + to the "<code>"view! {...}"</code>" from the last example: + " + </p> <details> <summary>"Expand for the rather verbose code example"</summary> - <div><pre><code class="language-rust">{example}</code></pre></div> + <div> + <pre> + <code class="language-rust">{example}</code> + </pre> + </div> </details> - <p>" - Given that multiple frameworks that will manipulate the DOM in their own and assume they are the - only source of truth is the problem - being demonstrated by Leptos in previous examples assuming - that nothing else would change the DOM for hydration. So if it is possible to use the JavaScript - library in a way that wouldn't cause unexpected DOM changes, then that can be a way to avoid - needing all these additional event listeners for working around the panics. - "</p> - <p>" - One thing to note is that this is a very simple example with a single Suspense (or Transition), so - if there are more than one of them and they have significantly different resolution timings, - calling that potentially indiscriminate JavaScript DOM manipulation function may require - additional care (e.g. needing to wait for all the events in a future before making the final call - to do make the invasive DOM manipulation). Let's look at one more similar example that use a - cheap workaround that may work for cases like integrating the simple JavaScript library here. - "</p> + <p> + " + Given that multiple frameworks that will manipulate the DOM in their own and assume they are the + only source of truth is the problem - being demonstrated by Leptos in previous examples assuming + that nothing else would change the DOM for hydration. So if it is possible to use the JavaScript + library in a way that wouldn't cause unexpected DOM changes, then that can be a way to avoid + needing all these additional event listeners for working around the panics. + " + </p> + <p> + " + One thing to note is that this is a very simple example with a single Suspense (or Transition), so + if there are more than one of them and they have significantly different resolution timings, + calling that potentially indiscriminate JavaScript DOM manipulation function may require + additional care (e.g. needing to wait for all the events in a future before making the final call + to do make the invasive DOM manipulation). Let's look at one more similar example that use a + cheap workaround that may work for cases like integrating the simple JavaScript library here. + " + </p> } } @@ -884,30 +1139,44 @@ fn WasmBindgenEffect() -> impl IntoView { view! { <h2>"Using "<code>"wasm-bindgen"</code>" with proper consideration, part 2"</h2> - <CodeDemoWasm mode=WasmDemo::RequestAnimationFrame/> - <p>" - This example simply uses "<code>"window.requestAnimationFrame()"</code>" (via the binding - available as "<code>"leptos::prelude::request_animation_frame"</code>") to delay the running of + <CodeDemoWasm mode=WasmDemo::RequestAnimationFrame /> + <p> + " + This example simply uses "<code>"window.requestAnimationFrame()"</code> + " (via the binding + available as "<code>"leptos::prelude::request_animation_frame"</code> + ") to delay the running of the highlighting by a tick so that both the hydration would complete for SSR, and that it would also delay highlighting call to after the suspend results are loaded onto the DOM. The Suspend for the dynamic code block is simply reduced to the following: - "</p> - <div><pre><code class="language-rust">{example}</code></pre></div> - <p>" - However, this method does have a drawback, which is that the inline code blocks will be processed - multiple times by this indiscriminate method (which "<code>"highlight.js"</code>" thankfully has a + " + </p> + <div> + <pre> + <code class="language-rust">{example}</code> + </pre> + </div> + <p> + " + However, this method does have a drawback, which is that the inline code blocks will be processed + multiple times by this indiscriminate method (which "<code>"highlight.js"</code> + " thankfully has a failsafe detection which avoids issues, but definitely don't count on this being the norm with JavaScript libraries). We could go back to the previous example where we use events to trigger for when the Suspend is resolved, but this will mean there needs to be some way to co-ordinate and wait for all of them to ensure the JavaScript library is only invoked once on the hydrated output. - "</p> - <p>" - If the JavaScript library provides an alternative API that does not involve this wrestling of the - DOM but does achieve the intended objectives is in fact available, it would definitely be the - better choice. Even better, make them available in Rust through "<code>"wasm-bindgen"</code>" so + " + </p> + <p> + " + If the JavaScript library provides an alternative API that does not involve this wrestling of the + DOM but does achieve the intended objectives is in fact available, it would definitely be the + better choice. Even better, make them available in Rust through " + <code>"wasm-bindgen"</code>" so that the relevant Leptos component may use them directly. In the next couple examples we will see how this idea may be put into practice. - "</p> + " + </p> } } @@ -931,7 +1200,9 @@ fn CodeInner(code: String, lang: String) -> impl IntoView { inner }; view! { - <pre><code inner_html=inner></code></pre> + <pre> + <code inner_html=inner></code> + </pre> } .into_any() } else { @@ -954,7 +1225,9 @@ fn CodeInner(code: String, lang: String) -> impl IntoView { }); }; view! { - <pre><code inner_html=inner></code></pre> + <pre> + <code inner_html=inner></code> + </pre> } .into_any() } @@ -966,18 +1239,20 @@ fn CodeDemoWasmInner() -> impl IntoView { let code_view = move || { Suspend::new(async move { code.await.map(|code| { - view! { - <CodeInner code=code lang="rust".to_string()/> - } + view! { <CodeInner code=code lang="rust".to_string() /> } }) }) }; view! { - <p>" - The following code examples are assigned via "<code>"inner_html"</code>" after processing through - the relevant/available API call depending on SSR/CSR, without using any "<code>"web_sys"</code>" + <p> + " + The following code examples are assigned via "<code>"inner_html"</code> + " after processing through + the relevant/available API call depending on SSR/CSR, without using any " + <code>"web_sys"</code>" events or DOM manipulation outside of Leptos. - "</p> + " + </p> <div id="code-demo"> <table> <thead> @@ -988,11 +1263,13 @@ fn CodeDemoWasmInner() -> impl IntoView { </thead> <tbody> <tr> - <td><CodeInner code=CH03_05A.to_string() lang="rust".to_string()/></td> <td> - <Suspense fallback=move || view! { <p>"Loading code example..."</p> }> - {code_view} - </Suspense> + <CodeInner code=CH03_05A.to_string() lang="rust".to_string() /> + </td> + <td> + <Suspense fallback=move || { + view! { <p>"Loading code example..."</p> } + }>{code_view}</Suspense> </td> </tr> </tbody> @@ -1022,40 +1299,52 @@ fn CodeInner(code: String, lang: String) -> impl IntoView { view! { <h2>"If possible, avoid DOM manipulation outside of Leptos"</h2> - <CodeDemoWasmInner/> - <p>" - Whenever possible, look for a way to use the target JavaScript library to produce the desired - markup without going through a global DOM manipulation can end up being much more straight-forward - to write when working in pure Rust code. More so if there is a server side counterpart, which - means the use of the module don't need the disambiguation within the component itself. A - simplified version of a component that will render a code block that gets highlighted under CSR - (and plain text under SSR) may look something like this: - "</p> - <CodeInner code lang/> - <p>" - In the above example, no additional "<code>"<script>"</code>" tags, post-hydration processing, + <CodeDemoWasmInner /> + <p> + " + Whenever possible, look for a way to use the target JavaScript library to produce the desired + markup without going through a global DOM manipulation can end up being much more straight-forward + to write when working in pure Rust code. More so if there is a server side counterpart, which + means the use of the module don't need the disambiguation within the component itself. A + simplified version of a component that will render a code block that gets highlighted under CSR + (and plain text under SSR) may look something like this: + " + </p> + <CodeInner code lang /> + <p> + " + In the above example, no additional "<code>"<script>"</code> + " tags, post-hydration processing, event listeners nor other DOM manipuation are needed, as the JavaScript function that converts a string to highlighted markup can be made from Rust through bindings generated with the use of - "<code>"wasm-bindgen"</code>" under CSR. As the highlight functionality isn't available under - SSR, the incoming code is simply processed using "<code>"html_escape::encode_text"</code>". - "</p> - <p>" - ... Well, if only it actually works, as there is a bit of an unexpected surprise during hydration. - During the hydration of the above code rendering component, the CSR specific pipeline kicks in and - calls "<code>"hljs::highlight"</code>", producing a different output that was assumed to trigger + "<code>"wasm-bindgen"</code> + " under CSR. As the highlight functionality isn't available under + SSR, the incoming code is simply processed using " + <code>"html_escape::encode_text"</code>". + " + </p> + <p> + " + ... Well, if only it actually works, as there is a bit of an unexpected surprise during hydration. + During the hydration of the above code rendering component, the CSR specific pipeline kicks in and + calls "<code>"hljs::highlight"</code> + ", producing a different output that was assumed to trigger a re-rendering. As hydration assumes the HTML rendered under SSR is isomorphic with CSR, a violation of this expectation (i.e. CSR rendering something entierly different) is not something it anticipates; the lack of re-rendering is in fact an optimization for performance reasons as it avoids unnecessary work. However in this instance, that isn't the desired behavior as the the syntax highlighting will not be shown as expected, and thankfully in this instance it does not result in a crash. - "</p> - <p>" - All that being said, the code is not doing what is desired, is there any way to go about this? - Fortunately, this is where effects comes in as it provides the intent to do something on the - client side, being able to function as an opt-in for CSR content to \"overwrite\" SSR content. - The next and final example will show how this should be done. - "</p> + " + </p> + <p> + " + All that being said, the code is not doing what is desired, is there any way to go about this? + Fortunately, this is where effects comes in as it provides the intent to do something on the + client side, being able to function as an opt-in for CSR content to \"overwrite\" SSR content. + The next and final example will show how this should be done. + " + </p> } } @@ -1085,49 +1374,67 @@ fn CodeInner(code: String, lang: String) -> impl IntoView { view! { <h2>"Corrected example using signal + effect (again)."</h2> - <CodeDemoWasmInner/> - <p>" - Since the previous example didn't quite get everything working due to the component here providing - different content between SSR and CSR, using client side signal and effect can opt-in the - difference to overwrite the SSR rendering when hydration is complete. This is pretty much the - identical approach as example 5 as it is the idiomatic solution. The improved version of the code - rendering component from the previous example may look something like the following: - "</p> - <CodeInner code lang/> - <p>" - With the use of effects, the expected final rendering after hydration and under CSR will be the - highlighted version as expected. As part of trial and error, the author previously tried to - workaround this issue by using events via "<code>"web_sys"</code>" hack around signal, but again, + <CodeDemoWasmInner /> + <p> + " + Since the previous example didn't quite get everything working due to the component here providing + different content between SSR and CSR, using client side signal and effect can opt-in the + difference to overwrite the SSR rendering when hydration is complete. This is pretty much the + identical approach as example 5 as it is the idiomatic solution. The improved version of the code + rendering component from the previous example may look something like the following: + " + </p> + <CodeInner code lang /> + <p> + " + With the use of effects, the expected final rendering after hydration and under CSR will be the + highlighted version as expected. As part of trial and error, the author previously tried to + workaround this issue by using events via "<code>"web_sys"</code> + " hack around signal, but again, using effects like so is a lot better for this particular library. - "</p> - <p>" - Given the difference between CSR and SSR, the two different renderings are disambiguated via the - use of "<code>"[cfg(feature = ...)]"</code>" for the available behavior. If there is a + " + </p> + <p> + " + Given the difference between CSR and SSR, the two different renderings are disambiguated via the + use of "<code>"[cfg(feature = ...)]"</code>" for the available behavior. If there is a corresponding API to provided highlighting markup under SSR, this feature gating would be managed - at the library level and the component would simply call the "<code>"highlight"</code>" function + at the library level and the component would simply call the "<code>"highlight"</code> + " function directly, resulting in both SSR/CSR rendering being fully isomorphic even with JavaScript disabled on the client. - "</p> - <p>" - To include the output of JavaScript code for SSR may be achieved in any of the following ways: - "</p> + " + </p> + <p> + " + To include the output of JavaScript code for SSR may be achieved in any of the following ways: + " + </p> <ul> - <li>" + <li> + " Run a JavaScript code in some JavaScript runtime such as Node.js, SpiderMonkey or Deno with the input, and return the collected output. - "</li> - <li>" + " + </li> + <li> + " Use a JavaScript engine as above but more directly through some kind of Rust bindings through packages such as "<code>"rusty_v8"</code>" or "<code>"mozjs"</code>". - "</li> - <li>" + " + </li> + <li> + " Or go the full WASM route - compile the required JavaScript into WASM and use that through Wasmtime on the server. - "</li> + " + </li> </ul> - <p>" - All of the above are very much outside the scope of this demo which is already showing the too - many ways to include JavaScript into a Leptos project. - "</p> + <p> + " + All of the above are very much outside the scope of this demo which is already showing the too + many ways to include JavaScript into a Leptos project. + " + </p> } } diff --git a/examples/hackernews/src/routes/story.rs b/examples/hackernews/src/routes/story.rs index f359a7e0a6..88afe3ad0e 100644 --- a/examples/hackernews/src/routes/story.rs +++ b/examples/hackernews/src/routes/story.rs @@ -23,24 +23,20 @@ pub fn Story() -> impl IntoView { None => Either::Left("Story not found."), Some(story) => { Either::Right(view! { - <Meta name="description" content=story.title.clone()/> + <Meta name="description" content=story.title.clone() /> <div class="item-view"> <div class="item-view-header"> <a href=story.url target="_blank"> <h1>{story.title}</h1> </a> <span class="host">"(" {story.domain} ")"</span> - {story - .user - .map(|user| { - view! { - <p class="meta"> - {story.points} " points | by " - <A href=format!("/users/{user}")>{user.clone()}</A> - {format!(" {}", story.time_ago)} - </p> - } - })} + <ShowLet some=move || story.user.clone() let:user> + <p class="meta"> + {story.points} " points | by " + <A href=format!("/users/{user}")>{user.clone()}</A> + {format!(" {}", story.time_ago)} + </p> + </ShowLet> </div> <div class="item-view-comments"> <p class="item-view-comments-header"> @@ -57,7 +53,7 @@ pub fn Story() -> impl IntoView { key=|comment| comment.id let:comment > - <Comment comment/> + <Comment comment /> </For> </ul> </div> @@ -110,8 +106,7 @@ pub fn Comment(comment: api::Comment) -> impl IntoView { </a> </div> {move || { - open - .get() + open.get() .then({ let comments = comment.comments.clone(); move || { @@ -122,7 +117,7 @@ pub fn Comment(comment: api::Comment) -> impl IntoView { key=|comment| comment.id let:comment > - <Comment comment/> + <Comment comment /> </For> </ul> } diff --git a/examples/hackernews_axum/src/routes/stories.rs b/examples/hackernews_axum/src/routes/stories.rs index eb2586b3a1..77904e6510 100644 --- a/examples/hackernews_axum/src/routes/stories.rs +++ b/examples/hackernews_axum/src/routes/stories.rs @@ -50,30 +50,41 @@ pub fn Stories() -> impl IntoView { <div class="news-view"> <div class="news-list-nav"> <span> - {move || if page() > 1 { - Either::Left(view! { - <a class="page-link" - href=move || format!("/{}?page={}", story_type(), page() - 1) - aria-label="Previous Page" - > - "< prev" - </a> - }) - } else { - Either::Right(view! { - <span class="page-link disabled" aria-hidden="true"> - "< prev" - </span> - }) + {move || { + if page() > 1 { + Either::Left( + view! { + <a + class="page-link" + href=move || { + format!("/{}?page={}", story_type(), page() - 1) + } + aria-label="Previous Page" + > + "< prev" + </a> + }, + ) + } else { + Either::Right( + view! { + <span class="page-link disabled" aria-hidden="true"> + "< prev" + </span> + }, + ) + } }} </span> <span>"page " {page}</span> <Suspense> - <span class="page-link" + <span + class="page-link" class:disabled=hide_more_link aria-hidden=hide_more_link > - <a href=move || format!("/{}?page={}", story_type(), page() + 1) + <a + href=move || format!("/{}?page={}", story_type(), page() + 1) aria-label="Next Page" > "more >" @@ -83,21 +94,17 @@ pub fn Stories() -> impl IntoView { </div> <main class="news-list"> <div> - <Transition - fallback=move || view! { <p>"Loading..."</p> } - set_pending - > - <Show when=move || stories.read().as_ref().map(Option::is_none).unwrap_or(false)> - > - <p>"Error loading stories."</p> - </Show> + <Transition fallback=move || view! { <p>"Loading..."</p> } set_pending> + <Show when=move || { + stories.read().as_ref().map(Option::is_none).unwrap_or(false) + }>> <p>"Error loading stories."</p></Show> <ul> <For each=move || stories.get().unwrap_or_default().unwrap_or_default() key=|story| story.id let:story > - <Story story/> + <Story story /> </For> </ul> </Transition> @@ -110,18 +117,20 @@ pub fn Stories() -> impl IntoView { #[component] fn Story(story: api::Story) -> impl IntoView { view! { - <li class="news-item"> + <li class="news-item"> <span class="score">{story.points}</span> <span class="title"> {if !story.url.starts_with("item?id=") { - Either::Left(view! { - <span> - <a href=story.url target="_blank" rel="noreferrer"> - {story.title.clone()} - </a> - <span class="host">"("{story.domain}")"</span> - </span> - }) + Either::Left( + view! { + <span> + <a href=story.url target="_blank" rel="noreferrer"> + {story.title.clone()} + </a> + <span class="host">"("{story.domain}")"</span> + </span> + }, + ) } else { let title = story.title.clone(); Either::Right(view! { <A href=format!("/stories/{}", story.id)>{title}</A> }) @@ -130,29 +139,40 @@ fn Story(story: api::Story) -> impl IntoView { <br /> <span class="meta"> {if story.story_type != "job" { - Either::Left(view! { - <span> - {"by "} - {story.user.map(|user| view ! { <A href=format!("/users/{user}")>{user.clone()}</A>})} - {format!(" {} | ", story.time_ago)} - <A href=format!("/stories/{}", story.id)> - {if story.comments_count.unwrap_or_default() > 0 { - format!("{} comments", story.comments_count.unwrap_or_default()) - } else { - "discuss".into() - }} - </A> - </span> - }) + Either::Left( + view! { + <span> + {"by "} <ShowLet some=move || story.user.clone() let:user> + <A href=format!("/users/{user}")>{user.clone()}</A> + </ShowLet> {format!(" {} | ", story.time_ago)} + <A href=format!( + "/stories/{}", + story.id, + )> + {if story.comments_count.unwrap_or_default() > 0 { + format!( + "{} comments", + story.comments_count.unwrap_or_default(), + ) + } else { + "discuss".into() + }} + </A> + </span> + }, + ) } else { let title = story.title.clone(); Either::Right(view! { <A href=format!("/item/{}", story.id)>{title}</A> }) }} </span> - {(story.story_type != "link").then(|| view! { - " " - <span class="label">{story.story_type}</span> - })} + {(story.story_type != "link") + .then(|| { + view! { + " " + <span class="label">{story.story_type}</span> + } + })} </li> } } diff --git a/leptos/src/show.rs b/leptos/src/show.rs index fec7288052..3dc1766ab1 100644 --- a/leptos/src/show.rs +++ b/leptos/src/show.rs @@ -1,6 +1,6 @@ use crate::{ children::{TypedChildrenFn, ViewFn}, - prelude::{FunctionMarker, SignalMarker}, + prelude::FunctionMarker, IntoView, }; use leptos_macro::component; @@ -91,7 +91,8 @@ pub trait IntoCondition<M> { fn into_condition(self) -> Condition; } -impl<S> IntoCondition<SignalMarker> for S +#[cfg(not(feature = "nightly"))] +impl<S> IntoCondition<crate::prelude::SignalMarker> for S where S: Get<Value = bool> + Send + Sync + 'static, { diff --git a/leptos/src/show_let.rs b/leptos/src/show_let.rs index bbaeaa4756..0416d10c2f 100644 --- a/leptos/src/show_let.rs +++ b/leptos/src/show_let.rs @@ -147,10 +147,12 @@ where } } +#[cfg(not(feature = "nightly"))] /// Marker type for creating an `OptionGetter` from a signal. /// Used so that the compiler doesn't complain about double implementations of the trait `IntoOptionGetter`. pub struct SignalMarker; +#[cfg(not(feature = "nightly"))] impl<T, S> IntoOptionGetter<T, SignalMarker> for S where S: Get<Value = Option<T>> + Clone + Send + Sync + 'static, From 1d42d99edc024ce0e5300c55e36c11d854ba6ea1 Mon Sep 17 00:00:00 2001 From: zakstucke <44890343+zakstucke@users.noreply.github.com> Date: Fri, 29 Aug 2025 02:50:33 +0300 Subject: [PATCH 05/91] Preserve owner in Actions and event listeners (#4267) * Preserve Owner for Action's, allowing context retrieval * Preserve owner for event listeners, allowing context retrieval * CI * Refactor --------- Co-authored-by: Zak Stucke <zakstucke@hotmail.c.uk> --- reactive_graph/src/actions/action.rs | 18 ++++++++++++++---- tachys/src/html/event.rs | 18 ++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/reactive_graph/src/actions/action.rs b/reactive_graph/src/actions/action.rs index 3ac5e1b165..4e05f65cd3 100644 --- a/reactive_graph/src/actions/action.rs +++ b/reactive_graph/src/actions/action.rs @@ -1,7 +1,7 @@ use crate::{ - computed::{ArcMemo, Memo}, + computed::{ArcMemo, Memo, ScopedFuture}, diagnostics::is_suppressing_resource_load, - owner::{ArcStoredValue, ArenaItem}, + owner::{ArcStoredValue, ArenaItem, Owner}, send_wrapper_ext::SendOption, signal::{ArcMappedSignal, ArcRwSignal, MappedSignal, RwSignal}, traits::{DefinedAt, Dispose, Get, GetUntracked, GetValue, Update, Write}, @@ -199,13 +199,19 @@ where I: Send + Sync, O: Send + Sync, { + let owner = Owner::current().unwrap_or_default(); ArcAction { in_flight: ArcRwSignal::new(0), input: ArcRwSignal::new(SendOption::new(None)), value: ArcRwSignal::new(SendOption::new(value)), version: Default::default(), dispatched: Default::default(), - action_fn: Arc::new(move |input| Box::pin(action_fn(input))), + action_fn: Arc::new(move |input| { + Box::pin( + owner + .with(|| ScopedFuture::new_untracked(action_fn(input))), + ) + }), #[cfg(any(debug_assertions, leptos_debuginfo))] defined_at: Location::caller(), } @@ -370,6 +376,7 @@ where F: Fn(&I) -> Fu + 'static, Fu: Future<Output = O> + 'static, { + let owner = Owner::current().unwrap_or_default(); let action_fn = SendWrapper::new(action_fn); ArcAction { in_flight: ArcRwSignal::new(0), @@ -378,7 +385,10 @@ where version: Default::default(), dispatched: Default::default(), action_fn: Arc::new(move |input| { - Box::pin(SendWrapper::new(action_fn(input))) + Box::pin(SendWrapper::new( + owner + .with(|| ScopedFuture::new_untracked(action_fn(input))), + )) }), #[cfg(any(debug_assertions, leptos_debuginfo))] defined_at: Location::caller(), diff --git a/tachys/src/html/event.rs b/tachys/src/html/event.rs index 362abc6a1f..1caace2bac 100644 --- a/tachys/src/html/event.rs +++ b/tachys/src/html/event.rs @@ -110,6 +110,8 @@ where { On { event, + #[cfg(feature = "reactive_graph")] + owner: reactive_graph::owner::Owner::current().unwrap_or_default(), cb: Some(SendWrapper::new(cb)), } } @@ -135,6 +137,8 @@ where /// An [`Attribute`] that adds an event listener to an element. pub struct On<E, F> { event: E, + #[cfg(feature = "reactive_graph")] + owner: reactive_graph::owner::Owner, cb: Option<SendWrapper<F>>, } @@ -146,6 +150,8 @@ where fn clone(&self) -> Self { Self { event: self.event.clone(), + #[cfg(feature = "reactive_graph")] + owner: self.owner.clone(), cb: self.cb.clone(), } } @@ -193,6 +199,10 @@ where let _tracing_guard = span.enter(); let ev = E::EventType::from(ev); + + #[cfg(feature = "reactive_graph")] + self.owner.with(|| cb.invoke(ev)); + #[cfg(not(feature = "reactive_graph"))] cb.invoke(ev); }) as Box<dyn FnMut(crate::renderer::types::Event)>; @@ -232,6 +242,10 @@ where let _tracing_guard = span.enter(); let ev = E::EventType::from(ev); + + #[cfg(feature = "reactive_graph")] + self.owner.with(|| cb.invoke(ev)); + #[cfg(not(feature = "reactive_graph"))] cb.invoke(ev); }) as Box<dyn FnMut(crate::renderer::types::Event)>; @@ -320,6 +334,8 @@ where fn into_cloneable(self) -> Self::Cloneable { On { cb: self.cb.map(|cb| SendWrapper::new(cb.take().into_shared())), + #[cfg(feature = "reactive_graph")] + owner: self.owner, event: self.event, } } @@ -327,6 +343,8 @@ where fn into_cloneable_owned(self) -> Self::CloneableOwned { On { cb: self.cb.map(|cb| SendWrapper::new(cb.take().into_shared())), + #[cfg(feature = "reactive_graph")] + owner: self.owner, event: self.event, } } From d63c78d6e95a4a8079e1734b13f94a3b058c75fe Mon Sep 17 00:00:00 2001 From: Spencer Ferris <3319370+spencewenski@users.noreply.github.com> Date: Fri, 29 Aug 2025 05:56:26 -0700 Subject: [PATCH 06/91] fix: set `Content-Type` header for server function errors (closes #4209) (#4215) --- server_fn/src/error.rs | 4 ++-- server_fn/src/lib.rs | 10 ++++++---- server_fn/src/middleware/mod.rs | 8 ++++++++ server_fn/src/response/actix.rs | 8 +++++++- server_fn/src/response/generic.rs | 7 +++++++ server_fn/src/response/http.rs | 7 +++++++ server_fn/src/response/mod.rs | 13 +++++++++++-- 7 files changed, 48 insertions(+), 9 deletions(-) diff --git a/server_fn/src/error.rs b/server_fn/src/error.rs index c8944d7b8b..565bf98e79 100644 --- a/server_fn/src/error.rs +++ b/server_fn/src/error.rs @@ -568,7 +568,7 @@ pub trait FromServerFnError: std::fmt::Debug + Sized + 'static { /// Converts a [`ServerFnErrorErr`] into the application-specific custom error type. fn from_server_fn_error(value: ServerFnErrorErr) -> Self; - /// Converts the custom error type to a [`String`]. + /// Serializes the custom error type to bytes, according to the encoding given by `Self::Encoding`. fn ser(&self) -> Bytes { Self::Encoder::encode(self).unwrap_or_else(|e| { Self::Encoder::encode(&Self::from_server_fn_error( @@ -581,7 +581,7 @@ pub trait FromServerFnError: std::fmt::Debug + Sized + 'static { }) } - /// Deserializes the custom error type from a [`&str`]. + /// Deserializes the custom error type, according to the encoding given by `Self::Encoding`. fn de(data: Bytes) -> Self { Self::Encoder::decode(data).unwrap_or_else(|e| { ServerFnErrorErr::Deserialization(e.to_string()).into_app_error() diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index 612b254c86..da47ffcfd0 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -307,16 +307,18 @@ pub trait ServerFn: Send + Sized { .await .map(|res| (res, None)) .unwrap_or_else(|e| { - ( + let mut response = <<Self as ServerFn>::Server as crate::Server< Self::Error, Self::InputStreamError, Self::OutputStreamError, >>::Response::error_response( Self::PATH, e.ser() - ), - Some(e), - ) + ); + let content_type = + <Self::Error as FromServerFnError>::Encoder::CONTENT_TYPE; + response.content_type(content_type); + (response, Some(e)) }); // if it accepts HTML, we'll redirect to the Referer diff --git a/server_fn/src/middleware/mod.rs b/server_fn/src/middleware/mod.rs index 508ab76340..609e5646ad 100644 --- a/server_fn/src/middleware/mod.rs +++ b/server_fn/src/middleware/mod.rs @@ -72,6 +72,10 @@ mod axum { let inner = self.call(req); Box::pin(async move { inner.await.unwrap_or_else(|e| { + // TODO: This does not set the Content-Type on the response. Doing so will + // require a breaking change in order to get the correct encoding from the + // error's `FromServerFnError::Encoder::CONTENT_TYPE` impl. + // Note: This only applies to middleware errors. let err = ser(ServerFnErrorErr::MiddlewareError(e.to_string())); Response::<Body>::error_response(&path, err) @@ -149,6 +153,10 @@ mod actix { let inner = self.call(req); Box::pin(async move { inner.await.unwrap_or_else(|e| { + // TODO: This does not set the Content-Type on the response. Doing so will + // require a breaking change in order to get the correct encoding from the + // error's `FromServerFnError::Encoder::CONTENT_TYPE` impl. + // Note: This only applies to middleware errors. let err = ser(ServerFnErrorErr::MiddlewareError(e.to_string())); ActixResponse::error_response(&path, err).take() diff --git a/server_fn/src/response/actix.rs b/server_fn/src/response/actix.rs index c823c6ac9e..749b290417 100644 --- a/server_fn/src/response/actix.rs +++ b/server_fn/src/response/actix.rs @@ -5,7 +5,7 @@ use crate::error::{ use actix_web::{ http::{ header, - header::{HeaderValue, LOCATION}, + header::{HeaderValue, CONTENT_TYPE, LOCATION}, StatusCode, }, HttpResponse, @@ -80,6 +80,12 @@ impl Res for ActixResponse { )) } + fn content_type(&mut self, content_type: &str) { + if let Ok(content_type) = HeaderValue::from_str(content_type) { + self.0.headers_mut().insert(CONTENT_TYPE, content_type); + } + } + fn redirect(&mut self, path: &str) { if let Ok(path) = HeaderValue::from_str(path) { *self.0.status_mut() = StatusCode::FOUND; diff --git a/server_fn/src/response/generic.rs b/server_fn/src/response/generic.rs index 6952dd1e81..3ed01e62a2 100644 --- a/server_fn/src/response/generic.rs +++ b/server_fn/src/response/generic.rs @@ -100,6 +100,13 @@ impl Res for Response<Body> { .unwrap() } + fn content_type(&mut self, content_type: &str) { + if let Ok(content_type) = HeaderValue::from_str(content_type) { + self.headers_mut() + .insert(header::CONTENT_TYPE, content_type); + } + } + fn redirect(&mut self, path: &str) { if let Ok(path) = HeaderValue::from_str(path) { self.headers_mut().insert(header::LOCATION, path); diff --git a/server_fn/src/response/http.rs b/server_fn/src/response/http.rs index 65e1b41832..e6f2842643 100644 --- a/server_fn/src/response/http.rs +++ b/server_fn/src/response/http.rs @@ -60,6 +60,13 @@ impl Res for Response<Body> { .unwrap() } + fn content_type(&mut self, content_type: &str) { + if let Ok(content_type) = HeaderValue::from_str(content_type) { + self.headers_mut() + .insert(header::CONTENT_TYPE, content_type); + } + } + fn redirect(&mut self, path: &str) { if let Ok(path) = HeaderValue::from_str(path) { self.headers_mut().insert(header::LOCATION, path); diff --git a/server_fn/src/response/mod.rs b/server_fn/src/response/mod.rs index 224713c149..41fa384542 100644 --- a/server_fn/src/response/mod.rs +++ b/server_fn/src/response/mod.rs @@ -37,9 +37,14 @@ where /// Represents the response as created by the server; pub trait Res { - /// Converts an error into a response, with a `500` status code and the error text as its body. + /// Converts an error into a response, with a `500` status code and the error as its body. fn error_response(path: &str, err: Bytes) -> Self; - + /// Set the `Content-Type` header for the response. + fn content_type(&mut self, #[allow(unused_variables)] content_type: &str) { + // TODO 0.9: remove this method and default implementation. It is only included here + // to allow setting the `Content-Type` header for error responses without requiring a + // semver-incompatible change. + } /// Redirect the response by setting a 302 code and Location header. fn redirect(&mut self, path: &str); } @@ -103,6 +108,10 @@ impl Res for BrowserMockRes { unreachable!() } + fn content_type(&mut self, _content_type: &str) { + unreachable!() + } + fn redirect(&mut self, _path: &str) { unreachable!() } From 549a7e9c22a7d88a31b8d0b19efe2936d2462184 Mon Sep 17 00:00:00 2001 From: zakstucke <44890343+zakstucke@users.noreply.github.com> Date: Fri, 29 Aug 2025 23:02:00 +0300 Subject: [PATCH 07/91] feat: impl `From<View<C>> for ViewFn where View<C>: Clone` (#4266) --- leptos/src/children.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/leptos/src/children.rs b/leptos/src/children.rs index d3e7880860..27f07ea11e 100644 --- a/leptos/src/children.rs +++ b/leptos/src/children.rs @@ -262,6 +262,16 @@ where } } +impl<C> From<View<C>> for ViewFn +where + C: Clone + Send + Sync + 'static, + View<C>: IntoAny, +{ + fn from(value: View<C>) -> Self { + Self(Arc::new(move || value.clone().into_any())) + } +} + impl ViewFn { /// Execute the wrapped function pub fn run(&self) -> AnyView { @@ -289,6 +299,16 @@ where } } +impl<C> From<View<C>> for ViewFnOnce +where + C: Send + Sync + 'static, + View<C>: IntoAny, +{ + fn from(value: View<C>) -> Self { + Self(Box::new(move || value.into_any())) + } +} + impl ViewFnOnce { /// Execute the wrapped function pub fn run(self) -> AnyView { From c7290c7b61c868623251011d0716bcc8d30c1a39 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Aug 2025 16:08:39 -0400 Subject: [PATCH 08/91] chore(deps): bump the rust-dependencies group across 1 directory with 33 updates (#4262) Bumps the rust-dependencies group with 32 updates in the / directory: | Package | From | To | | --- | --- | --- | | [serde_json](https://github.com/serde-rs/json) | `1.0.142` | `1.0.143` | | [typed-builder](https://github.com/idanarye/rust-typed-builder) | `0.21.0` | `0.21.2` | | [thiserror](https://github.com/dtolnay/thiserror) | `2.0.12` | `2.0.16` | | [indexmap](https://github.com/indexmap-rs/indexmap) | `2.10.0` | `2.11.0` | | [cfg-if](https://github.com/rust-lang/cfg-if) | `1.0.1` | `1.0.3` | | [proc-macro2](https://github.com/dtolnay/proc-macro2) | `1.0.96` | `1.0.101` | | [syn](https://github.com/dtolnay/syn) | `2.0.104` | `2.0.106` | | [async-trait](https://github.com/dtolnay/async-trait) | `0.1.88` | `0.1.89` | | [anyhow](https://github.com/dtolnay/anyhow) | `1.0.98` | `1.0.99` | | [prettyplease](https://github.com/dtolnay/prettyplease) | `0.2.36` | `0.2.37` | | [inventory](https://github.com/dtolnay/inventory) | `0.3.20` | `0.3.21` | | [config](https://github.com/rust-cli/config-rs) | `0.15.13` | `0.15.14` | | [regex](https://github.com/rust-lang/regex) | `1.11.1` | `1.11.2` | | [tempfile](https://github.com/Stebalien/tempfile) | `3.20.0` | `3.21.0` | | [percent-encoding](https://github.com/servo/rust-url) | `2.3.1` | `2.3.2` | | [hyper](https://github.com/hyperium/hyper) | `1.6.0` | `1.7.0` | | [reqwest](https://github.com/seanmonstar/reqwest) | `0.12.22` | `0.12.23` | | [actix-http](https://github.com/actix/actix-web) | `3.11.0` | `3.11.1` | | [bitflags](https://github.com/bitflags/bitflags) | `2.9.1` | `2.9.3` | | [brotli](https://github.com/dropbox/rust-brotli) | `8.0.1` | `8.0.2` | | [cc](https://github.com/rust-lang/cc-rs) | `1.2.32` | `1.2.34` | | [form_urlencoded](https://github.com/servo/rust-url) | `1.2.1` | `1.2.2` | | [idna](https://github.com/servo/rust-url) | `1.0.3` | `1.1.0` | | [io-uring](https://github.com/tokio-rs/io-uring) | `0.7.9` | `0.7.10` | | [jobserver](https://github.com/rust-lang/jobserver-rs) | `0.1.33` | `0.1.34` | | [regex-automata](https://github.com/rust-lang/regex) | `0.4.9` | `0.4.10` | | [regex-lite](https://github.com/rust-lang/regex) | `0.1.6` | `0.1.7` | | [regex-syntax](https://github.com/rust-lang/regex) | `0.8.5` | `0.8.6` | | [scc](https://github.com/wvwwvwwv/scalable-concurrent-containers) | `2.3.4` | `2.4.0` | | [tinyvec](https://github.com/Lokathor/tinyvec) | `1.9.0` | `1.10.0` | | [winapi-util](https://github.com/BurntSushi/winapi-util) | `0.1.9` | `0.1.10` | | [winnow](https://github.com/winnow-rs/winnow) | `0.7.12` | `0.7.13` | Updates `serde_json` from 1.0.142 to 1.0.143 - [Release notes](https://github.com/serde-rs/json/releases) - [Commits](https://github.com/serde-rs/json/compare/v1.0.142...v1.0.143) Updates `typed-builder` from 0.21.0 to 0.21.2 - [Release notes](https://github.com/idanarye/rust-typed-builder/releases) - [Changelog](https://github.com/idanarye/rust-typed-builder/blob/master/CHANGELOG.md) - [Commits](https://github.com/idanarye/rust-typed-builder/compare/v0.21.0...v0.21.2) Updates `thiserror` from 2.0.12 to 2.0.16 - [Release notes](https://github.com/dtolnay/thiserror/releases) - [Commits](https://github.com/dtolnay/thiserror/compare/2.0.12...2.0.16) Updates `indexmap` from 2.10.0 to 2.11.0 - [Changelog](https://github.com/indexmap-rs/indexmap/blob/main/RELEASES.md) - [Commits](https://github.com/indexmap-rs/indexmap/compare/2.10.0...2.11.0) Updates `cfg-if` from 1.0.1 to 1.0.3 - [Release notes](https://github.com/rust-lang/cfg-if/releases) - [Changelog](https://github.com/rust-lang/cfg-if/blob/main/CHANGELOG.md) - [Commits](https://github.com/rust-lang/cfg-if/compare/v1.0.1...v1.0.3) Updates `proc-macro2` from 1.0.96 to 1.0.101 - [Release notes](https://github.com/dtolnay/proc-macro2/releases) - [Commits](https://github.com/dtolnay/proc-macro2/compare/1.0.96...1.0.101) Updates `syn` from 2.0.104 to 2.0.106 - [Release notes](https://github.com/dtolnay/syn/releases) - [Commits](https://github.com/dtolnay/syn/compare/2.0.104...2.0.106) Updates `async-trait` from 0.1.88 to 0.1.89 - [Release notes](https://github.com/dtolnay/async-trait/releases) - [Commits](https://github.com/dtolnay/async-trait/compare/0.1.88...0.1.89) Updates `typed-builder-macro` from 0.21.0 to 0.21.2 - [Release notes](https://github.com/idanarye/rust-typed-builder/releases) - [Changelog](https://github.com/idanarye/rust-typed-builder/blob/master/CHANGELOG.md) - [Commits](https://github.com/idanarye/rust-typed-builder/compare/v0.21.0...v0.21.2) Updates `anyhow` from 1.0.98 to 1.0.99 - [Release notes](https://github.com/dtolnay/anyhow/releases) - [Commits](https://github.com/dtolnay/anyhow/compare/1.0.98...1.0.99) Updates `prettyplease` from 0.2.36 to 0.2.37 - [Release notes](https://github.com/dtolnay/prettyplease/releases) - [Commits](https://github.com/dtolnay/prettyplease/compare/0.2.36...0.2.37) Updates `inventory` from 0.3.20 to 0.3.21 - [Release notes](https://github.com/dtolnay/inventory/releases) - [Commits](https://github.com/dtolnay/inventory/compare/0.3.20...0.3.21) Updates `config` from 0.15.13 to 0.15.14 - [Changelog](https://github.com/rust-cli/config-rs/blob/main/CHANGELOG.md) - [Commits](https://github.com/rust-cli/config-rs/compare/v0.15.13...v0.15.14) Updates `regex` from 1.11.1 to 1.11.2 - [Release notes](https://github.com/rust-lang/regex/releases) - [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-lang/regex/compare/1.11.1...1.11.2) Updates `tempfile` from 3.20.0 to 3.21.0 - [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md) - [Commits](https://github.com/Stebalien/tempfile/commits) Updates `percent-encoding` from 2.3.1 to 2.3.2 - [Release notes](https://github.com/servo/rust-url/releases) - [Commits](https://github.com/servo/rust-url/commits) Updates `hyper` from 1.6.0 to 1.7.0 - [Release notes](https://github.com/hyperium/hyper/releases) - [Changelog](https://github.com/hyperium/hyper/blob/master/CHANGELOG.md) - [Commits](https://github.com/hyperium/hyper/compare/v1.6.0...v1.7.0) Updates `reqwest` from 0.12.22 to 0.12.23 - [Release notes](https://github.com/seanmonstar/reqwest/releases) - [Changelog](https://github.com/seanmonstar/reqwest/blob/master/CHANGELOG.md) - [Commits](https://github.com/seanmonstar/reqwest/compare/v0.12.22...v0.12.23) Updates `actix-http` from 3.11.0 to 3.11.1 - [Release notes](https://github.com/actix/actix-web/releases) - [Changelog](https://github.com/actix/actix-web/blob/master/CHANGES.md) - [Commits](https://github.com/actix/actix-web/compare/http-v3.11.0...http-v3.11.1) Updates `bitflags` from 2.9.1 to 2.9.3 - [Release notes](https://github.com/bitflags/bitflags/releases) - [Changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md) - [Commits](https://github.com/bitflags/bitflags/compare/2.9.1...2.9.3) Updates `brotli` from 8.0.1 to 8.0.2 - [Release notes](https://github.com/dropbox/rust-brotli/releases) - [Commits](https://github.com/dropbox/rust-brotli/commits/8.0.2) Updates `cc` from 1.2.32 to 1.2.34 - [Release notes](https://github.com/rust-lang/cc-rs/releases) - [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md) - [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.32...cc-v1.2.34) Updates `form_urlencoded` from 1.2.1 to 1.2.2 - [Release notes](https://github.com/servo/rust-url/releases) - [Commits](https://github.com/servo/rust-url/compare/v1.2.1...v1.2.2) Updates `idna` from 1.0.3 to 1.1.0 - [Release notes](https://github.com/servo/rust-url/releases) - [Commits](https://github.com/servo/rust-url/commits) Updates `io-uring` from 0.7.9 to 0.7.10 - [Commits](https://github.com/tokio-rs/io-uring/commits) Updates `jobserver` from 0.1.33 to 0.1.34 - [Commits](https://github.com/rust-lang/jobserver-rs/compare/0.1.33...0.1.34) Updates `regex-automata` from 0.4.9 to 0.4.10 - [Release notes](https://github.com/rust-lang/regex/releases) - [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-lang/regex/compare/regex-automata-0.4.9...regex-automata-0.4.10) Updates `regex-lite` from 0.1.6 to 0.1.7 - [Release notes](https://github.com/rust-lang/regex/releases) - [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-lang/regex/compare/regex-lite-0.1.6...regex-lite-0.1.7) Updates `regex-syntax` from 0.8.5 to 0.8.6 - [Release notes](https://github.com/rust-lang/regex/releases) - [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-lang/regex/compare/regex-syntax-0.8.5...regex-syntax-0.8.6) Updates `scc` from 2.3.4 to 2.4.0 - [Changelog](https://github.com/wvwwvwwv/scalable-concurrent-containers/blob/main/CHANGELOG.md) - [Commits](https://github.com/wvwwvwwv/scalable-concurrent-containers/commits) Updates `tinyvec` from 1.9.0 to 1.10.0 - [Changelog](https://github.com/Lokathor/tinyvec/blob/main/CHANGELOG.md) - [Commits](https://github.com/Lokathor/tinyvec/compare/v1.9.0...v1.10.0) Updates `winapi-util` from 0.1.9 to 0.1.10 - [Commits](https://github.com/BurntSushi/winapi-util/compare/0.1.9...0.1.10) Updates `winnow` from 0.7.12 to 0.7.13 - [Changelog](https://github.com/winnow-rs/winnow/blob/main/CHANGELOG.md) - [Commits](https://github.com/winnow-rs/winnow/compare/v0.7.12...v0.7.13) --- updated-dependencies: - dependency-name: serde_json dependency-version: 1.0.143 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: typed-builder dependency-version: 0.21.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: thiserror dependency-version: 2.0.16 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: indexmap dependency-version: 2.11.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: rust-dependencies - dependency-name: cfg-if dependency-version: 1.0.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: proc-macro2 dependency-version: 1.0.101 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: syn dependency-version: 2.0.106 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: async-trait dependency-version: 0.1.89 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: typed-builder-macro dependency-version: 0.21.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: anyhow dependency-version: 1.0.99 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: prettyplease dependency-version: 0.2.37 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: inventory dependency-version: 0.3.21 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: config dependency-version: 0.15.14 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: regex dependency-version: 1.11.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: tempfile dependency-version: 3.21.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: rust-dependencies - dependency-name: percent-encoding dependency-version: 2.3.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: hyper dependency-version: 1.7.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: rust-dependencies - dependency-name: reqwest dependency-version: 0.12.23 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: actix-http dependency-version: 3.11.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: bitflags dependency-version: 2.9.3 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: brotli dependency-version: 8.0.2 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: cc dependency-version: 1.2.34 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: form_urlencoded dependency-version: 1.2.2 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: idna dependency-version: 1.1.0 dependency-type: indirect update-type: version-update:semver-minor dependency-group: rust-dependencies - dependency-name: io-uring dependency-version: 0.7.10 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: jobserver dependency-version: 0.1.34 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: regex-automata dependency-version: 0.4.10 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: regex-lite dependency-version: 0.1.7 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: regex-syntax dependency-version: 0.8.6 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: scc dependency-version: 2.4.0 dependency-type: indirect update-type: version-update:semver-minor dependency-group: rust-dependencies - dependency-name: tinyvec dependency-version: 1.10.0 dependency-type: indirect update-type: version-update:semver-minor dependency-group: rust-dependencies - dependency-name: winapi-util dependency-version: 0.1.10 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: winnow dependency-version: 0.7.13 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 286 +++++++++++++++++++++++++++-------------------------- Cargo.toml | 36 +++---- 2 files changed, 165 insertions(+), 157 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0b331dde0b..4c06d66f3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,9 +44,9 @@ dependencies = [ [[package]] name = "actix-http" -version = "3.11.0" +version = "3.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44dfe5c9e0004c623edc65391dfd51daa201e7e30ebd9c9bedf873048ec32bc2" +checksum = "44cceded2fb55f3c4b67068fa64962e2ca59614edc5b03167de9ff82ae803da0" dependencies = [ "actix-codec", "actix-rt", @@ -88,7 +88,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -205,7 +205,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -270,7 +270,7 @@ dependencies = [ "futures-lite", "glib", "serial_test", - "thiserror 2.0.12", + "thiserror 2.0.16", "tokio", "tracing", "wasm-bindgen-futures", @@ -279,9 +279,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" [[package]] name = "async-executor" @@ -333,7 +333,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -344,13 +344,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.88" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -362,6 +362,12 @@ dependencies = [ "critical-section", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "attribute-derive" version = "0.10.3" @@ -373,7 +379,7 @@ dependencies = [ "manyhow", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -389,7 +395,7 @@ dependencies = [ "proc-macro2", "quote", "quote-use", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -485,9 +491,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" [[package]] name = "block-buffer" @@ -500,9 +506,9 @@ dependencies = [ [[package]] name = "brotli" -version = "8.0.1" +version = "8.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9991eea70ea4f293524138648e41ee89b0b2b12ddef3b255effa43c8056e0e0d" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -545,7 +551,7 @@ checksum = "efb7846e0cb180355c2dec69e721edafa36919850f1a9f52ffba4ebc0393cb71" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -577,9 +583,9 @@ checksum = "5d07aa9a93b00c76f71bc35d598bed923f6d4f3a9ca5c24b7737ae1a292841c0" [[package]] name = "cc" -version = "1.2.32" +version = "1.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2352e5597e9c544d5e6d9c95190d5d27738ade584fa8db0a16e130e5c2b5296e" +checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" dependencies = [ "jobserver", "libc", @@ -598,9 +604,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "cfg_aliases" @@ -641,7 +647,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" dependencies = [ - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] @@ -657,7 +663,7 @@ dependencies = [ "serde-lite", "serde-wasm-bindgen", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.16", "wasm-bindgen", ] @@ -678,9 +684,9 @@ dependencies = [ [[package]] name = "config" -version = "0.15.13" +version = "0.15.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b1eb4fb07bc7f012422df02766c7bd5971effb894f573865642f06fa3265440" +checksum = "aa4092bf3922a966e2bd74640b80f36c73eaa7251a4fd0fbcda1f8a4de401352" dependencies = [ "convert_case 0.6.0", "pathdiff", @@ -865,7 +871,7 @@ checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -878,7 +884,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -898,7 +904,7 @@ checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "unicode-xid", ] @@ -920,7 +926,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1058,9 +1064,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -1135,7 +1141,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1255,7 +1261,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1494,19 +1500,21 @@ dependencies = [ [[package]] name = "hyper" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", + "futures-core", "http 1.3.1", "http-body", "httparse", "httpdate", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -1563,7 +1571,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.0", "tokio", "tower-service", "tracing", @@ -1657,9 +1665,9 @@ dependencies = [ [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -1684,9 +1692,9 @@ checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" [[package]] name = "indexmap" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" dependencies = [ "equivalent", "hashbrown 0.15.4", @@ -1711,18 +1719,18 @@ checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8" [[package]] name = "inventory" -version = "0.3.20" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab08d7cd2c5897f2c949e5383ea7c7db03fb19130ffcfbf7eda795137ae3cb83" +checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" dependencies = [ "rustversion", ] [[package]] name = "io-uring" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" dependencies = [ "bitflags", "cfg-if", @@ -1762,9 +1770,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jobserver" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ "getrandom 0.3.3", "libc", @@ -1817,7 +1825,7 @@ dependencies = [ "server_fn", "slotmap", "tachys", - "thiserror 2.0.12", + "thiserror 2.0.16", "throw_error", "tokio", "tokio-test", @@ -1839,7 +1847,7 @@ dependencies = [ "http 1.3.1", "proc-macro-error", "server_fn_macro 0.6.15", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1899,7 +1907,7 @@ dependencies = [ "serde", "temp-env", "tempfile", - "thiserror 2.0.12", + "thiserror 2.0.16", "tokio", "typed-builder", ] @@ -1933,7 +1941,7 @@ dependencies = [ "quote", "rstml", "serde", - "syn 2.0.104", + "syn 2.0.106", "walkdir", ] @@ -1973,7 +1981,7 @@ dependencies = [ "serde", "server_fn", "server_fn_macro 0.8.7", - "syn 2.0.104", + "syn 2.0.106", "tracing", "trybuild", "typed-builder", @@ -2011,7 +2019,7 @@ dependencies = [ "rustc_version", "send_wrapper", "tachys", - "thiserror 2.0.12", + "thiserror 2.0.16", "tracing", "url", "wasm-bindgen", @@ -2028,7 +2036,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2124,7 +2132,7 @@ dependencies = [ "manyhow-macros", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2174,7 +2182,7 @@ checksum = "d6c74ab4f1a0c0ab045260ee4727b23c00cc17e5eff5095262d08eef8c3c8d49" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2253,7 +2261,7 @@ checksum = "f2e3795a5d2da581a8b252fec6022eee01aea10161a4d1bf237d4cbe47f7e988" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2317,7 +2325,7 @@ version = "0.2.1" dependencies = [ "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] @@ -2349,7 +2357,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2417,9 +2425,9 @@ checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project" @@ -2438,7 +2446,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2498,12 +2506,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.36" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2558,7 +2566,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2574,9 +2582,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.96" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "beef09f85ae72cea1ef96ba6870c51e6382ebfa4f0e85b643459331f3daa5be0" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] @@ -2589,7 +2597,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "version_check", "yansi", ] @@ -2611,7 +2619,7 @@ checksum = "ca414edb151b4c8d125c12566ab0d74dc9cdba36fb80eb7b848c15f495fd32d1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2628,7 +2636,7 @@ dependencies = [ "rustc-hash 2.1.1", "rustls", "socket2 0.5.10", - "thiserror 2.0.12", + "thiserror 2.0.16", "tokio", "tracing", "web-time", @@ -2649,7 +2657,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.12", + "thiserror 2.0.16", "tinyvec", "tracing", "web-time", @@ -2697,7 +2705,7 @@ dependencies = [ "proc-macro-utils", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2761,7 +2769,7 @@ dependencies = [ "send_wrapper", "serde", "slotmap", - "thiserror 2.0.12", + "thiserror 2.0.16", "tokio", "tokio-test", "tracing", @@ -2795,7 +2803,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2809,9 +2817,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.1" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" dependencies = [ "aho-corasick", "memchr", @@ -2821,9 +2829,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" dependencies = [ "aho-corasick", "memchr", @@ -2832,15 +2840,15 @@ dependencies = [ [[package]] name = "regex-lite" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" +checksum = "943f41321c63ef1c92fd763bfe054d2668f7f225a5c29f0105903dc2fc04ba30" [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] name = "rend" @@ -2853,9 +2861,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.22" +version = "0.12.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" dependencies = [ "base64", "bytes", @@ -2937,7 +2945,7 @@ checksum = "4270433626cffc9c4c1d3707dd681f2a2718d3d7b09ad754bec137acecda8d22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2972,9 +2980,9 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.104", + "syn 2.0.106", "syn_derive", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] @@ -3075,9 +3083,9 @@ dependencies = [ [[package]] name = "scc" -version = "2.3.4" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22b2d775fb28f245817589471dd49c5edf64237f4a19d10ce9a92ff4651a27f4" +checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" dependencies = [ "sdd", ] @@ -3168,7 +3176,7 @@ checksum = "7ce26a84e3d8d10853301cf6a75c58132b8f5d5e8fee65949ea8dd7758d6760b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -3190,14 +3198,14 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "serde_json" -version = "1.0.142" +version = "1.0.143" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" dependencies = [ "itoa", "memchr", @@ -3223,7 +3231,7 @@ checksum = "f3faaf9e727533a19351a43cc5a8de957372163c7d35cc48c90b75cdda13c352" dependencies = [ "percent-encoding", "serde", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] @@ -3278,7 +3286,7 @@ checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -3315,7 +3323,7 @@ dependencies = [ "serde_json", "serde_qs", "server_fn_macro_default", - "thiserror 2.0.12", + "thiserror 2.0.16", "throw_error", "tokio", "tokio-tungstenite 0.27.0", @@ -3340,7 +3348,7 @@ dependencies = [ "convert_case 0.6.0", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "xxhash-rust", ] @@ -3353,7 +3361,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.104", + "syn 2.0.106", "xxhash-rust", ] @@ -3362,7 +3370,7 @@ name = "server_fn_macro_default" version = "0.8.5" dependencies = [ "server_fn_macro 0.8.7", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -3437,7 +3445,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f62f06db0370222f7f498ef478fce9f8df5828848d1d3517e3331936d7074f55" dependencies = [ "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -3517,9 +3525,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.104" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -3535,7 +3543,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -3555,7 +3563,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -3634,15 +3642,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.20.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -3665,11 +3673,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.16", ] [[package]] @@ -3680,18 +3688,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -3744,9 +3752,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -3785,7 +3793,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -4019,7 +4027,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -4065,7 +4073,7 @@ dependencies = [ "log", "rand", "sha1", - "thiserror 2.0.12", + "thiserror 2.0.16", "utf-8", ] @@ -4082,28 +4090,28 @@ dependencies = [ "log", "rand", "sha1", - "thiserror 2.0.12", + "thiserror 2.0.16", "utf-8", ] [[package]] name = "typed-builder" -version = "0.21.0" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce63bcaf7e9806c206f7d7b9c1f38e0dce8bb165a80af0898161058b19248534" +checksum = "fef81aec2ca29576f9f6ae8755108640d0a86dd3161b2e8bca6cfa554e98f77d" dependencies = [ "typed-builder-macro", ] [[package]] name = "typed-builder-macro" -version = "0.21.0" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60d8d828da2a3d759d3519cdf29a5bac49c77d039ad36d0782edadbf9cd5415b" +checksum = "1ecb9ecf7799210407c14a8cfdfe0173365780968dc57973ed082211958e0b18" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -4262,7 +4270,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "wasm-bindgen-shared", ] @@ -4297,7 +4305,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4332,7 +4340,7 @@ checksum = "17d5042cc5fa009658f9a7333ef24291b1291a25b6382dd68862a7f3b969f69b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -4365,7 +4373,7 @@ dependencies = [ "digest", "quote", "sha2", - "syn 2.0.104", + "syn 2.0.106", "wasm-bindgen", ] @@ -4400,9 +4408,9 @@ dependencies = [ [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" dependencies = [ "windows-sys 0.52.0", ] @@ -4564,9 +4572,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] @@ -4618,7 +4626,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "synstructure", ] @@ -4639,7 +4647,7 @@ checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -4659,7 +4667,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "synstructure", ] @@ -4699,7 +4707,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 7bfc4c2c33..6fa6601425 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,12 +77,12 @@ wasm_split_macros = { path = "./wasm_split_macros", version = "0.1.2" } async-once-cell = { default-features = false, version = "0.5.3" } 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.142" } +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.0" } -thiserror = { default-features = false, version = "2.0.12" } +typed-builder = { default-features = false, version = "0.21.2" } +thiserror = { default-features = false, version = "2.0.16" } wasm-bindgen = { default-features = false, version = "0.2.100" } -indexmap = { default-features = false, version = "2.9.0" } +indexmap = { default-features = false, version = "2.11.0" } rstml = { default-features = false, version = "0.12.1" } rustc_version = { default-features = false, version = "0.4.1" } guardian = { default-features = false, version = "1.3.0" } @@ -102,15 +102,15 @@ gloo-net = { default-features = false, version = "0.6.0" } url = { default-features = false, version = "2.5.4" } tokio = { default-features = false, version = "1.47.1" } base64 = { default-features = false, version = "0.22.1" } -cfg-if = { default-features = false, version = "1.0.0" } +cfg-if = { default-features = false, version = "1.0.3" } wasm-bindgen-futures = { default-features = false, version = "0.4.50" } tower = { default-features = false, version = "0.5.2" } -proc-macro2 = { default-features = false, version = "1.0.96" } +proc-macro2 = { default-features = false, version = "1.0.101" } serde = { default-features = false, version = "1.0.219" } parking_lot = { default-features = false, version = "0.12.4" } axum = { default-features = false, version = "0.8.4" } serde_qs = { default-features = false, version = "0.15.0" } -syn = { default-features = false, version = "2.0.104" } +syn = { default-features = false, version = "2.0.106" } xxhash-rust = { default-features = false, version = "0.8.15" } paste = { default-features = false, version = "1.0.15" } quote = { default-features = false, version = "1.0.40" } @@ -122,16 +122,16 @@ tokio-tungstenite = { default-features = false, version = "0.27.0" } 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.88" } +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.98" } +anyhow = { default-features = false, version = "1.0.99" } walkdir = { default-features = false, version = "2.5.0" } actix-ws = { default-features = false, version = "0.3.0" } tower-http = { default-features = false, version = "0.6.4" } -prettyplease = { default-features = false, version = "0.2.36" } -inventory = { default-features = false, version = "0.3.20" } -config = { default-features = false, version = "0.15.13" } +prettyplease = { default-features = false, version = "0.2.37" } +inventory = { default-features = false, version = "0.3.21" } +config = { default-features = false, version = "0.15.14" } camino = { default-features = false, version = "1.1.11" } ciborium = { default-features = false, version = "0.2.2" } multer = { default-features = false, version = "3.1.0" } @@ -144,24 +144,24 @@ temp-env = { default-features = false, version = "0.3.6" } uuid = { default-features = false, version = "1.18.0" } bytes = { default-features = false, version = "1.10.1" } http = { default-features = false, version = "1.3.1" } -regex = { default-features = false, version = "1.11.1" } +regex = { default-features = false, version = "1.11.2" } drain_filter_polyfill = { default-features = false, version = "0.1.3" } -tempfile = { default-features = false, version = "3.20.0" } +tempfile = { default-features = false, version = "3.21.0" } futures-lite = { default-features = false, version = "2.6.1" } log = { default-features = false, version = "0.4.27" } -percent-encoding = { default-features = false, version = "2.3.1" } +percent-encoding = { default-features = false, version = "2.3.2" } async-executor = { default-features = false, version = "1.13.2" } const-str = { default-features = false, version = "0.6.4" } http-body-util = { default-features = false, version = "0.1.3" } -hyper = { default-features = false, version = "1.6.0" } +hyper = { default-features = false, version = "1.7.0" } postcard = { default-features = false, version = "1.1.3" } rmp-serde = { default-features = false, version = "1.3.0" } -reqwest = { default-features = false, version = "0.12.22" } +reqwest = { default-features = false, version = "0.12.23" } tower-layer = { default-features = false, version = "0.3.3" } attribute-derive = { default-features = false, version = "0.10.3" } insta = { default-features = false, version = "1.43.1" } codee = { default-features = false, version = "0.3.0" } -actix-http = { default-features = false, version = "3.11.0" } +actix-http = { default-features = false, version = "3.11.1" } wasm-bindgen-test = { default-features = false, version = "0.3.50" } rustversion = { default-features = false, version = "1.0.22" } getrandom = { default-features = false, version = "0.3.3" } From e321ed97d70b817e6aa783f97f5e811bdf789bc4 Mon Sep 17 00:00:00 2001 From: zakstucke <44890343+zakstucke@users.noreply.github.com> Date: Fri, 29 Aug 2025 23:09:41 +0300 Subject: [PATCH 09/91] feat: `impl From<RwSignal/ReadSignal/Memo> for ArcSignal` (#4258) --- reactive_graph/src/wrappers.rs | 42 ++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/reactive_graph/src/wrappers.rs b/reactive_graph/src/wrappers.rs index 272b269e0a..b91a27b285 100644 --- a/reactive_graph/src/wrappers.rs +++ b/reactive_graph/src/wrappers.rs @@ -257,6 +257,20 @@ pub mod read { } } + impl<T, S> From<ReadSignal<T, S>> for ArcSignal<T, S> + where + S: Storage<ArcReadSignal<T>> + Storage<T>, + { + #[track_caller] + fn from(value: ReadSignal<T, S>) -> Self { + Self { + inner: SignalTypes::ReadSignal(value.into()), + #[cfg(any(debug_assertions, leptos_debuginfo))] + defined_at: std::panic::Location::caller(), + } + } + } + impl<T: Send + Sync> From<ArcRwSignal<T>> for ArcSignal<T, SyncStorage> { #[track_caller] fn from(value: ArcRwSignal<T>) -> Self { @@ -268,6 +282,20 @@ pub mod read { } } + impl<T, S> From<RwSignal<T, S>> for ArcSignal<T, S> + where + S: Storage<ArcRwSignal<T>> + Storage<ArcReadSignal<T>> + Storage<T>, + { + #[track_caller] + fn from(value: RwSignal<T, S>) -> Self { + Self { + inner: SignalTypes::ReadSignal(value.read_only().into()), + #[cfg(any(debug_assertions, leptos_debuginfo))] + defined_at: std::panic::Location::caller(), + } + } + } + impl<T, S> From<ArcMemo<T, S>> for ArcSignal<T, S> where S: Storage<T>, @@ -282,6 +310,20 @@ pub mod read { } } + impl<T, S> From<Memo<T, S>> for ArcSignal<T, S> + where + S: Storage<ArcMemo<T, S>> + Storage<T>, + { + #[track_caller] + fn from(value: Memo<T, S>) -> Self { + Self { + inner: SignalTypes::Memo(value.into()), + #[cfg(any(debug_assertions, leptos_debuginfo))] + defined_at: std::panic::Location::caller(), + } + } + } + impl<T, S> DefinedAt for ArcSignal<T, S> where S: Storage<T>, From fc4fd65ef6e8c652f141348a27ea11f79e984a13 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Sat, 30 Aug 2025 06:46:36 -0400 Subject: [PATCH 10/91] Revert "fix: correctly parse unquoted text with punctuation in stable (closes #4137) (#4238)" This reverts commit 99c3d8f9e98f8c18f5c02b9c3d017240c4eb5053. --- leptos_macro/Cargo.toml | 4 +--- leptos_macro/src/lib.rs | 9 +-------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/leptos_macro/Cargo.toml b/leptos_macro/Cargo.toml index fc9d5c112a..574bd61263 100644 --- a/leptos_macro/Cargo.toml +++ b/leptos_macro/Cargo.toml @@ -24,9 +24,7 @@ proc-macro-error2 = { default-features = false, workspace = true } proc-macro2 = { workspace = true, default-features = true } quote = { workspace = true, default-features = true } syn = { features = ["full"], workspace = true, default-features = true } -rstml = { workspace = true, default-features = true, features = [ - "rawtext-stable-hack", -] } +rstml = { workspace = true, default-features = true } leptos_hot_reload = { workspace = true } server_fn_macro = { workspace = true } convert_case = { workspace = true, default-features = true } diff --git a/leptos_macro/src/lib.rs b/leptos_macro/src/lib.rs index e07c94198e..999f4fab4d 100644 --- a/leptos_macro/src/lib.rs +++ b/leptos_macro/src/lib.rs @@ -323,14 +323,7 @@ fn view_macro_impl(tokens: TokenStream, template: bool) -> TokenStream { .chain(tokens) .collect() }; - let macro_call_pattern = if let Some(class) = &global_class { - quote!(view! { class = #class, %% }) - } else { - quote!(view! {%%}) - }; - let config = rstml::ParserConfig::default() - .recover_block(true) - .macro_call_pattern(macro_call_pattern); + let config = rstml::ParserConfig::default().recover_block(true); let parser = rstml::Parser::new(config); let (mut nodes, errors) = parser.parse_recoverable(tokens).split_vec(); let errors = errors.into_iter().map(|e| e.emit_as_expr_tokens()); From 13f87f10dd9eb39ab504325780d7610c3f9655a7 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Sat, 30 Aug 2025 06:47:57 -0400 Subject: [PATCH 11/91] leptos_macro-v0.8.8 --- leptos_macro/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/leptos_macro/Cargo.toml b/leptos_macro/Cargo.toml index 574bd61263..141b49ea6c 100644 --- a/leptos_macro/Cargo.toml +++ b/leptos_macro/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "leptos_macro" -version = "0.8.7" +version = "0.8.8" authors = ["Greg Johnston"] license = "MIT" repository = "https://github.com/leptos-rs/leptos" From ce7dff4706bbd0780fdfffb1ae55c6b5bc60dd9d Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Sat, 30 Aug 2025 06:53:42 -0400 Subject: [PATCH 12/91] chore: update leptos_macro version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 6fa6601425..de6df7a630 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,7 +55,7 @@ leptos_config = { path = "./leptos_config", version = "0.8.7" } leptos_dom = { path = "./leptos_dom", version = "0.8.6" } leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.5" } leptos_integration_utils = { path = "./integrations/utils", version = "0.8.5" } -leptos_macro = { path = "./leptos_macro", version = "0.8.7" } +leptos_macro = { path = "./leptos_macro", version = "0.8.8" } leptos_router = { path = "./router", version = "0.8.6" } leptos_router_macro = { path = "./router_macro", version = "0.8.5" } leptos_server = { path = "./leptos_server", version = "0.8.5" } From 92c11ee447bb03ae00000bad00ff8378607a13a3 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 30 Aug 2025 11:25:00 +0000 Subject: [PATCH 13/91] [autofix.ci] apply automated fixes --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 4c06d66f3f..6c151db55b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1960,7 +1960,7 @@ dependencies = [ [[package]] name = "leptos_macro" -version = "0.8.7" +version = "0.8.8" dependencies = [ "attribute-derive", "cfg-if", From 51e9a39156769cfa41fbe8df214bfdd94c268c4f Mon Sep 17 00:00:00 2001 From: zakstucke <44890343+zakstucke@users.noreply.github.com> Date: Sun, 31 Aug 2025 19:30:16 +0300 Subject: [PATCH 14/91] feat: standardize ScopedFuture::new_untracked like untrack() and untrack_with_diagnostics() (#4269) --- reactive_graph/src/actions/action.rs | 15 +++---- .../async_derived/arc_async_derived.rs | 7 +-- .../src/computed/async_derived/mod.rs | 44 +++++++++++++++++++ 3 files changed, 55 insertions(+), 11 deletions(-) diff --git a/reactive_graph/src/actions/action.rs b/reactive_graph/src/actions/action.rs index 4e05f65cd3..938fd04ee2 100644 --- a/reactive_graph/src/actions/action.rs +++ b/reactive_graph/src/actions/action.rs @@ -1,6 +1,7 @@ use crate::{ computed::{ArcMemo, Memo, ScopedFuture}, diagnostics::is_suppressing_resource_load, + graph::untrack, owner::{ArcStoredValue, ArenaItem, Owner}, send_wrapper_ext::SendOption, signal::{ArcMappedSignal, ArcRwSignal, MappedSignal, RwSignal}, @@ -207,10 +208,9 @@ where version: Default::default(), dispatched: Default::default(), action_fn: Arc::new(move |input| { - Box::pin( - owner - .with(|| ScopedFuture::new_untracked(action_fn(input))), - ) + Box::pin(owner.with(|| { + ScopedFuture::new_untracked(untrack(|| action_fn(input))) + })) }), #[cfg(any(debug_assertions, leptos_debuginfo))] defined_at: Location::caller(), @@ -385,10 +385,9 @@ where version: Default::default(), dispatched: Default::default(), action_fn: Arc::new(move |input| { - Box::pin(SendWrapper::new( - owner - .with(|| ScopedFuture::new_untracked(action_fn(input))), - )) + Box::pin(SendWrapper::new(owner.with(|| { + ScopedFuture::new_untracked(untrack(|| action_fn(input))) + }))) }), #[cfg(any(debug_assertions, leptos_debuginfo))] defined_at: Location::caller(), diff --git a/reactive_graph/src/computed/async_derived/arc_async_derived.rs b/reactive_graph/src/computed/async_derived/arc_async_derived.rs index affa685774..c967fbc9eb 100644 --- a/reactive_graph/src/computed/async_derived/arc_async_derived.rs +++ b/reactive_graph/src/computed/async_derived/arc_async_derived.rs @@ -521,9 +521,10 @@ impl<T: 'static> ArcAsyncDerived<T> { { let fun = move || { let fut = fun(); - let fut = ScopedFuture::new_untracked(async move { - SendOption::new(Some(fut.await)) - }); + let fut = + ScopedFuture::new_untracked_with_diagnostics(async move { + SendOption::new(Some(fut.await)) + }); #[cfg(feature = "sandboxed-arenas")] let fut = Sandboxed::new(fut); fut diff --git a/reactive_graph/src/computed/async_derived/mod.rs b/reactive_graph/src/computed/async_derived/mod.rs index 6b43dbd6f6..8d911e9389 100644 --- a/reactive_graph/src/computed/async_derived/mod.rs +++ b/reactive_graph/src/computed/async_derived/mod.rs @@ -54,11 +54,55 @@ impl<Fut> ScopedFuture<Fut> { fut, } } + + #[doc(hidden)] + #[track_caller] + pub fn new_untracked_with_diagnostics( + fut: Fut, + ) -> ScopedFutureUntrackedWithDiagnostics<Fut> { + let owner = Owner::current().unwrap_or_default(); + ScopedFutureUntrackedWithDiagnostics { + owner, + observer: None, + fut, + } + } } impl<Fut: Future> Future for ScopedFuture<Fut> { type Output = Fut::Output; + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { + let this = self.project(); + this.owner.with(|| { + #[cfg(debug_assertions)] + let _maybe_guard = if this.observer.is_none() { + Some(crate::diagnostics::SpecialNonReactiveZone::enter()) + } else { + None + }; + this.observer.with_observer(|| this.fut.poll(cx)) + }) + } +} + +pin_project! { + /// A [`Future`] wrapper that sets the [`Owner`] and [`Observer`] before polling the inner + /// `Future`, output of [`ScopedFuture::new_untracked_with_diagnostics`]. + /// + /// In leptos 0.9 this will be replaced with `ScopedFuture` itself. + #[derive(Clone)] + pub struct ScopedFutureUntrackedWithDiagnostics<Fut> { + owner: Owner, + observer: Option<AnySubscriber>, + #[pin] + fut: Fut, + } +} + +impl<Fut: Future> Future for ScopedFutureUntrackedWithDiagnostics<Fut> { + type Output = Fut::Output; + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { let this = self.project(); this.owner From 347b0fc4655f4c541064b0b496fb8f90c9191b17 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Mon, 1 Sep 2025 07:41:54 -0400 Subject: [PATCH 15/91] fix: clear old attributes when replacing a Vec<AnyAttribute> (closes #4268) (#4270) --- tachys/src/html/attribute/any_attribute.rs | 75 ++++++++++++++++++---- tachys/src/html/attribute/custom.rs | 8 ++- tachys/src/html/attribute/mod.rs | 49 +++++++++++++- tachys/src/html/class.rs | 6 +- tachys/src/html/directive.rs | 8 ++- tachys/src/html/element/inner_html.rs | 6 +- tachys/src/html/event.rs | 5 ++ tachys/src/html/node_ref.rs | 9 ++- tachys/src/html/property.rs | 10 ++- tachys/src/html/style.rs | 8 ++- tachys/src/reactive_graph/bind.rs | 7 +- tachys/src/view/any_view.rs | 12 +++- tachys/src/view/either.rs | 12 +++- tachys/src/view/static_types.rs | 7 +- 14 files changed, 197 insertions(+), 25 deletions(-) diff --git a/tachys/src/html/attribute/any_attribute.rs b/tachys/src/html/attribute/any_attribute.rs index b86b3e6635..0b45d1089b 100644 --- a/tachys/src/html/attribute/any_attribute.rs +++ b/tachys/src/html/attribute/any_attribute.rs @@ -1,6 +1,10 @@ use super::{Attribute, NextAttribute}; -use crate::erased::{Erased, ErasedLocal}; -use std::{any::TypeId, fmt::Debug}; +use crate::{ + erased::{Erased, ErasedLocal}, + html::attribute::NamedAttributeKey, + renderer::{dom::Element, Rndr}, +}; +use std::{any::TypeId, fmt::Debug, mem}; #[cfg(feature = "ssr")] use std::{future::Future, pin::Pin}; @@ -25,6 +29,7 @@ pub struct AnyAttribute { resolve: fn(Erased) -> Pin<Box<dyn Future<Output = AnyAttribute> + Send>>, #[cfg(feature = "ssr")] dry_resolve: fn(&mut Erased), + keys: fn(&Erased) -> Vec<NamedAttributeKey>, } impl Clone for AnyAttribute { @@ -44,6 +49,7 @@ pub struct AnyAttributeState { type_id: TypeId, state: ErasedLocal, el: crate::renderer::types::Element, + keys: Vec<NamedAttributeKey>, } /// Converts an [`Attribute`] into [`AnyAttribute`]. @@ -84,6 +90,7 @@ where ) -> AnyAttributeState { AnyAttributeState { type_id: TypeId::of::<T>(), + keys: value.get_ref::<T>().keys(), state: ErasedLocal::new(value.into_inner::<T>().build(&el)), el, } @@ -96,6 +103,7 @@ where ) -> AnyAttributeState { AnyAttributeState { type_id: TypeId::of::<T>(), + keys: value.get_ref::<T>().keys(), state: ErasedLocal::new( value.into_inner::<T>().hydrate::<true>(&el), ), @@ -110,6 +118,7 @@ where ) -> AnyAttributeState { AnyAttributeState { type_id: TypeId::of::<T>(), + keys: value.get_ref::<T>().keys(), state: ErasedLocal::new( value.into_inner::<T>().hydrate::<true>(&el), ), @@ -140,6 +149,12 @@ where async move {value.into_inner::<T>().resolve().await.into_any_attr()}.boxed() } + fn keys<T: Attribute + 'static>( + value: &Erased, + ) -> Vec<NamedAttributeKey> { + value.get_ref::<T>().keys() + } + let value = self.into_cloneable_owned(); AnyAttribute { type_id: TypeId::of::<T::CloneableOwned>(), @@ -158,6 +173,7 @@ where resolve: resolve::<T::CloneableOwned>, #[cfg(feature = "ssr")] dry_resolve: dry_resolve::<T::CloneableOwned>, + keys: keys::<T::CloneableOwned>, } } } @@ -268,6 +284,10 @@ impl Attribute for AnyAttribute { enabled." ); } + + fn keys(&self) -> Vec<NamedAttributeKey> { + (self.keys)(&self.value) + } } impl NextAttribute for Vec<AnyAttribute> { @@ -286,7 +306,7 @@ impl Attribute for Vec<AnyAttribute> { const MIN_LENGTH: usize = 0; type AsyncOutput = Vec<AnyAttribute>; - type State = Vec<AnyAttributeState>; + type State = (Element, Vec<AnyAttributeState>); type Cloneable = Vec<AnyAttribute>; type CloneableOwned = Vec<AnyAttribute>; @@ -321,13 +341,19 @@ impl Attribute for Vec<AnyAttribute> { ) -> Self::State { #[cfg(feature = "hydrate")] if FROM_SERVER { - self.into_iter() - .map(|attr| attr.hydrate::<true>(el)) - .collect() + ( + el.clone(), + self.into_iter() + .map(|attr| attr.hydrate::<true>(el)) + .collect(), + ) } else { - self.into_iter() - .map(|attr| attr.hydrate::<false>(el)) - .collect() + ( + el.clone(), + self.into_iter() + .map(|attr| attr.hydrate::<false>(el)) + .collect(), + ) } #[cfg(not(feature = "hydrate"))] { @@ -340,13 +366,34 @@ impl Attribute for Vec<AnyAttribute> { } fn build(self, el: &crate::renderer::types::Element) -> Self::State { - self.into_iter().map(|attr| attr.build(el)).collect() + ( + el.clone(), + self.into_iter().map(|attr| attr.build(el)).collect(), + ) } fn rebuild(self, state: &mut Self::State) { - for (attr, state) in self.into_iter().zip(state.iter_mut()) { - attr.rebuild(state) + let (el, state) = state; + for old in mem::take(state) { + for key in old.keys { + match key { + NamedAttributeKey::InnerHtml => { + Rndr::set_inner_html(&old.el, ""); + } + NamedAttributeKey::Property(prop_name) => { + Rndr::set_property( + &old.el, + &prop_name, + &wasm_bindgen::JsValue::UNDEFINED, + ); + } + NamedAttributeKey::Attribute(key) => { + Rndr::remove_attribute(&old.el, &key); + } + } + } } + *state = self.into_iter().map(|s| s.build(el)).collect(); } fn into_cloneable(self) -> Self::Cloneable { @@ -385,4 +432,8 @@ impl Attribute for Vec<AnyAttribute> { enabled." ); } + + fn keys(&self) -> Vec<NamedAttributeKey> { + self.iter().flat_map(|s| s.keys()).collect() + } } diff --git a/tachys/src/html/attribute/custom.rs b/tachys/src/html/attribute/custom.rs index eedde99c00..6810aaee66 100644 --- a/tachys/src/html/attribute/custom.rs +++ b/tachys/src/html/attribute/custom.rs @@ -4,7 +4,7 @@ use super::{ use crate::{ html::attribute::{ maybe_next_attr_erasure_macros::next_attr_combine, Attribute, - AttributeValue, + AttributeValue, NamedAttributeKey, }, view::{add_attr::AddAnyAttr, Position, ToTemplate}, }; @@ -112,6 +112,12 @@ where value: self.value.resolve().await, } } + + fn keys(&self) -> Vec<NamedAttributeKey> { + vec![NamedAttributeKey::Attribute( + self.key.as_ref().to_string().into(), + )] + } } impl<K, V> NextAttribute for CustomAttr<K, V> diff --git a/tachys/src/html/attribute/mod.rs b/tachys/src/html/attribute/mod.rs index 920a1330f3..b07730aa4f 100644 --- a/tachys/src/html/attribute/mod.rs +++ b/tachys/src/html/attribute/mod.rs @@ -15,7 +15,7 @@ pub use key::*; use maybe_next_attr_erasure_macros::{ next_attr_combine, next_attr_output_type, }; -use std::{fmt::Debug, future::Future}; +use std::{borrow::Cow, fmt::Debug, future::Future}; pub use value::*; /// Defines an attribute: anything that can modify an element. @@ -75,6 +75,25 @@ pub trait Attribute: NextAttribute + Send { /// “Resolves” this into a type that is not waiting for any asynchronous data. fn resolve(self) -> impl Future<Output = Self::AsyncOutput> + Send; + + /// Returns a set of attribute keys, associated with this attribute, if any. + /// + /// This is only used to manage the removal of type-erased attributes, when needed. + fn keys(&self) -> Vec<NamedAttributeKey> { + // TODO: remove default implementation in 0.9, or fix this whole approach + // by making it easier to remove attributes + vec![] + } +} + +/// An attribute key can be used to remove an attribute from an element. +pub enum NamedAttributeKey { + /// An ordinary attribute. + Attribute(Cow<'static, str>), + /// A DOM property. + Property(Cow<'static, str>), + /// The `inner_html` pseudo-attribute. + InnerHtml, } /// Adds another attribute to this one, returning a new attribute. @@ -133,6 +152,10 @@ impl Attribute for () { fn dry_resolve(&mut self) {} async fn resolve(self) -> Self::AsyncOutput {} + + fn keys(&self) -> Vec<NamedAttributeKey> { + vec![] + } } impl NextAttribute for () { @@ -249,6 +272,10 @@ where async fn resolve(self) -> Self::AsyncOutput { Attr(self.0, self.1.resolve().await) } + + fn keys(&self) -> Vec<NamedAttributeKey> { + vec![NamedAttributeKey::Attribute(K::KEY.into())] + } } impl<K, V> NextAttribute for Attr<K, V> @@ -353,6 +380,14 @@ macro_rules! impl_attr_for_tuples { $($ty.resolve()),* ) } + + fn keys(&self) -> Vec<NamedAttributeKey> { + #[allow(non_snake_case)] + let ($first, $($ty,)*) = &self; + let mut buf = $first.keys(); + $(buf.extend($ty.keys());)* + buf + } } impl<$first, $($ty),*> NextAttribute for ($first, $($ty,)*) @@ -462,6 +497,14 @@ macro_rules! impl_attr_for_tuples_truncate_additional { $($ty.resolve()),* ) } + + fn keys(&self) -> Vec<NamedAttributeKey> { + #[allow(non_snake_case)] + let ($first, $($ty,)*) = &self; + let mut buf = $first.keys(); + $(buf.extend($ty.keys());)* + buf + } } impl<$first, $($ty),*> NextAttribute for ($first, $($ty,)*) @@ -538,6 +581,10 @@ where async fn resolve(self) -> Self::AsyncOutput { (self.0.resolve().await,) } + + fn keys(&self) -> Vec<NamedAttributeKey> { + self.0.keys() + } } impl<A> NextAttribute for (A,) diff --git a/tachys/src/html/class.rs b/tachys/src/html/class.rs index f67b4774ad..ce3bf70c9d 100644 --- a/tachys/src/html/class.rs +++ b/tachys/src/html/class.rs @@ -1,6 +1,6 @@ use super::attribute::{ maybe_next_attr_erasure_macros::next_attr_output_type, Attribute, - NextAttribute, + NamedAttributeKey, NextAttribute, }; use crate::{ html::attribute::maybe_next_attr_erasure_macros::next_attr_combine, @@ -97,6 +97,10 @@ where class: self.class.resolve().await, } } + + fn keys(&self) -> Vec<NamedAttributeKey> { + vec![NamedAttributeKey::Attribute("class".into())] + } } impl<C> NextAttribute for Class<C> diff --git a/tachys/src/html/directive.rs b/tachys/src/html/directive.rs index e824c044d8..b38af28fc3 100644 --- a/tachys/src/html/directive.rs +++ b/tachys/src/html/directive.rs @@ -3,7 +3,9 @@ use super::attribute::{ NextAttribute, }; use crate::{ - html::attribute::maybe_next_attr_erasure_macros::next_attr_combine, + html::attribute::{ + maybe_next_attr_erasure_macros::next_attr_combine, NamedAttributeKey, + }, prelude::AddAnyAttr, view::{Position, ToTemplate}, }; @@ -160,6 +162,10 @@ where async fn resolve(self) -> Self::AsyncOutput { self } + + fn keys(&self) -> Vec<NamedAttributeKey> { + vec![] + } } impl<T, D, P> NextAttribute for Directive<T, D, P> diff --git a/tachys/src/html/element/inner_html.rs b/tachys/src/html/element/inner_html.rs index 72d92803f3..90d8d503e3 100644 --- a/tachys/src/html/element/inner_html.rs +++ b/tachys/src/html/element/inner_html.rs @@ -4,7 +4,7 @@ use crate::{ maybe_next_attr_erasure_macros::{ next_attr_combine, next_attr_output_type, }, - Attribute, NextAttribute, + Attribute, NamedAttributeKey, NextAttribute, }, renderer::Rndr, view::add_attr::AddAnyAttr, @@ -105,6 +105,10 @@ where value: self.value.resolve().await, } } + + fn keys(&self) -> Vec<NamedAttributeKey> { + vec![NamedAttributeKey::InnerHtml] + } } impl<T> NextAttribute for InnerHtml<T> diff --git a/tachys/src/html/event.rs b/tachys/src/html/event.rs index 1caace2bac..4235069be0 100644 --- a/tachys/src/html/event.rs +++ b/tachys/src/html/event.rs @@ -1,6 +1,7 @@ use crate::{ html::attribute::{ maybe_next_attr_erasure_macros::next_attr_combine, Attribute, + NamedAttributeKey, }, renderer::{CastFrom, RemoveEventHandler, Rndr}, view::{Position, ToTemplate}, @@ -360,6 +361,10 @@ where async fn resolve(self) -> Self::AsyncOutput { self } + + fn keys(&self) -> Vec<NamedAttributeKey> { + vec![] + } } impl<E, F> NextAttribute for On<E, F> diff --git a/tachys/src/html/node_ref.rs b/tachys/src/html/node_ref.rs index 7ccd71bc3f..4b847e5948 100644 --- a/tachys/src/html/node_ref.rs +++ b/tachys/src/html/node_ref.rs @@ -7,7 +7,10 @@ use super::{ }; use crate::{ html::{ - attribute::maybe_next_attr_erasure_macros::next_attr_combine, + attribute::{ + maybe_next_attr_erasure_macros::next_attr_combine, + NamedAttributeKey, + }, element::HtmlElement, }, prelude::Render, @@ -112,6 +115,10 @@ where async fn resolve(self) -> Self::AsyncOutput { self } + + fn keys(&self) -> Vec<NamedAttributeKey> { + vec![] + } } impl<E, C> NextAttribute for NodeRefAttr<E, C> diff --git a/tachys/src/html/property.rs b/tachys/src/html/property.rs index c899818945..ac94a1808e 100644 --- a/tachys/src/html/property.rs +++ b/tachys/src/html/property.rs @@ -3,7 +3,9 @@ use super::attribute::{ NextAttribute, }; use crate::{ - html::attribute::maybe_next_attr_erasure_macros::next_attr_combine, + html::attribute::{ + maybe_next_attr_erasure_macros::next_attr_combine, NamedAttributeKey, + }, renderer::Rndr, view::{Position, ToTemplate}, }; @@ -124,6 +126,12 @@ where async fn resolve(self) -> Self::AsyncOutput { self } + + fn keys(&self) -> Vec<NamedAttributeKey> { + vec![NamedAttributeKey::Property( + self.key.as_ref().to_string().into(), + )] + } } impl<K, P> NextAttribute for Property<K, P> diff --git a/tachys/src/html/style.rs b/tachys/src/html/style.rs index 20a172d2b3..ed1f7b54ac 100644 --- a/tachys/src/html/style.rs +++ b/tachys/src/html/style.rs @@ -5,7 +5,9 @@ use super::attribute::{ #[cfg(all(feature = "nightly", rustc_nightly))] use crate::view::static_types::Static; use crate::{ - html::attribute::maybe_next_attr_erasure_macros::next_attr_combine, + html::attribute::{ + maybe_next_attr_erasure_macros::next_attr_combine, NamedAttributeKey, + }, renderer::{dom::CssStyleDeclaration, Rndr}, view::{Position, ToTemplate}, }; @@ -100,6 +102,10 @@ where style: self.style.resolve().await, } } + + fn keys(&self) -> Vec<NamedAttributeKey> { + vec![NamedAttributeKey::Attribute("style".into())] + } } impl<S> NextAttribute for Style<S> diff --git a/tachys/src/reactive_graph/bind.rs b/tachys/src/reactive_graph/bind.rs index b52ff8836a..1cee2b6fa3 100644 --- a/tachys/src/reactive_graph/bind.rs +++ b/tachys/src/reactive_graph/bind.rs @@ -5,7 +5,8 @@ use crate::{ maybe_next_attr_erasure_macros::{ next_attr_combine, next_attr_output_type, }, - Attribute, AttributeKey, AttributeValue, NextAttribute, + Attribute, AttributeKey, AttributeValue, NamedAttributeKey, + NextAttribute, }, event::{change, input, on}, property::{prop, IntoProperty}, @@ -275,6 +276,10 @@ where async fn resolve(self) -> Self::AsyncOutput { self } + + fn keys(&self) -> Vec<NamedAttributeKey> { + vec![] + } } impl<Key, T, R, W> NextAttribute for Bind<Key, T, R, W> diff --git a/tachys/src/view/any_view.rs b/tachys/src/view/any_view.rs index 8cfaa34c67..dbf0eaf6a1 100644 --- a/tachys/src/view/any_view.rs +++ b/tachys/src/view/any_view.rs @@ -699,7 +699,15 @@ impl Render for AnyViewWithAttrs { fn rebuild(self, state: &mut Self::State) { self.view.rebuild(&mut state.view); - self.attrs.rebuild(&mut state.attrs); + + let elements = state.elements(); + // FIXME this seems wrong but I think the previous version was also broken! + if let Some(element) = elements.first() { + self.attrs.rebuild(&mut ( + element.clone(), + std::mem::take(&mut state.attrs), + )); + } } } @@ -820,7 +828,7 @@ impl AddAnyAttr for AnyViewWithAttrs { } } -/// wip +/// State for any view with attributes spread onto it. pub struct AnyViewWithAttrsState { view: AnyViewState, attrs: Vec<AnyAttributeState>, diff --git a/tachys/src/view/either.rs b/tachys/src/view/either.rs index 2b5d5d5b2f..51a35e6b8a 100644 --- a/tachys/src/view/either.rs +++ b/tachys/src/view/either.rs @@ -3,7 +3,10 @@ use super::{ Render, RenderHtml, }; use crate::{ - html::attribute::{any_attribute::AnyAttribute, Attribute, NextAttribute}, + html::attribute::{ + any_attribute::AnyAttribute, Attribute, NamedAttributeKey, + NextAttribute, + }, hydration::Cursor, ssr::StreamBuilder, }; @@ -264,6 +267,13 @@ where Either::Right(right) => Either::Right(right.resolve().await), } } + + fn keys(&self) -> Vec<NamedAttributeKey> { + match self { + Either::Left(left) => left.keys(), + Either::Right(right) => right.keys(), + } + } } impl<A, B> RenderHtml for Either<A, B> diff --git a/tachys/src/view/static_types.rs b/tachys/src/view/static_types.rs index 2791a6ce5c..fa63109427 100644 --- a/tachys/src/view/static_types.rs +++ b/tachys/src/view/static_types.rs @@ -8,7 +8,8 @@ use crate::{ maybe_next_attr_erasure_macros::{ next_attr_combine, next_attr_output_type, }, - Attribute, AttributeKey, AttributeValue, NextAttribute, + Attribute, AttributeKey, AttributeValue, NamedAttributeKey, + NextAttribute, }, hydration::Cursor, renderer::{CastFrom, Rndr}, @@ -111,6 +112,10 @@ where async fn resolve(self) -> Self::AsyncOutput { self } + + fn keys(&self) -> Vec<NamedAttributeKey> { + vec![NamedAttributeKey::Attribute(K::KEY.into())] + } } impl<K, const V: &'static str> NextAttribute for StaticAttr<K, V> From 85719bcb47aad13bdfaed48f709b2096676c87d2 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Wed, 3 Sep 2025 08:33:38 -0400 Subject: [PATCH 16/91] fix: correctly propagate visibility on lazy functions (closes #4256) (#4259) --- wasm_split_macros/src/lib.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/wasm_split_macros/src/lib.rs b/wasm_split_macros/src/lib.rs index 7cd3774303..0ccc26dcf5 100644 --- a/wasm_split_macros/src/lib.rs +++ b/wasm_split_macros/src/lib.rs @@ -85,6 +85,7 @@ pub fn wasm_split(args: TokenStream, input: TokenStream) -> TokenStream { 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(); @@ -147,7 +148,8 @@ pub fn wasm_split(args: TokenStream, input: TokenStream) -> TokenStream { } #[allow(non_snake_case)] - #wrapper_sig { + #(#attrs)* + #wrapper_pub #wrapper_sig { #(#attrs)* #[allow(improper_ctypes_definitions)] #[allow(non_snake_case)] @@ -162,7 +164,7 @@ pub fn wasm_split(args: TokenStream, input: TokenStream) -> TokenStream { #[doc(hidden)] #[allow(non_snake_case)] - pub async fn #preload_name() { + #wrapper_pub async fn #preload_name() { ::leptos::wasm_split_helpers::ensure_loaded(&#split_loader_ident).await.unwrap(); } } From 66688277fb3617fcfd346e1c6daa927bb269d851 Mon Sep 17 00:00:00 2001 From: Raffaele Fontana <78023528+rpfontana@users.noreply.github.com> Date: Wed, 3 Sep 2025 14:34:08 +0200 Subject: [PATCH 17/91] docs: fix broken link in suspense (#4276) --- leptos/src/suspense_component.rs | 4 ++-- leptos/src/transition.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/leptos/src/suspense_component.rs b/leptos/src/suspense_component.rs index be72797043..e1e76b7cbc 100644 --- a/leptos/src/suspense_component.rs +++ b/leptos/src/suspense_component.rs @@ -32,12 +32,12 @@ use tachys::{ }; use throw_error::ErrorHookFuture; -/// If any [`Resource`](leptos_reactive::Resource) is read in the `children` of this +/// If any [`Resource`](crate::prelude::Resource) is read in the `children` of this /// component, it will show the `fallback` while they are loading. Once all are resolved, /// it will render the `children`. /// /// Each time one of the resources is loading again, it will fall back. To keep the current -/// children instead, use [Transition](crate::Transition). +/// children instead, use [Transition](crate::prelude::Transition). /// /// Note that the `children` will be rendered initially (in order to capture the fact that /// those resources are read under the suspense), so you cannot assume that resources read diff --git a/leptos/src/transition.rs b/leptos/src/transition.rs index f595544098..c886824275 100644 --- a/leptos/src/transition.rs +++ b/leptos/src/transition.rs @@ -16,11 +16,11 @@ use reactive_graph::{ use slotmap::{DefaultKey, SlotMap}; use tachys::reactive_graph::OwnedView; -/// If any [`Resource`](leptos_reactive::Resource) is read in the `children` of this +/// If any [`Resource`](crate::prelude::Resource) is read in the `children` of this /// component, it will show the `fallback` while they are loading. Once all are resolved, /// it will render the `children`. /// -/// Unlike [`Suspense`](crate::Suspense), this will not fall +/// Unlike [`Suspense`](crate::prelude::Suspense), this will not fall /// back to the `fallback` state if there are further changes after the initial load. /// /// Note that the `children` will be rendered initially (in order to capture the fact that From f082af8c20537b82e998940ef8d9040da1c73706 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Wed, 10 Sep 2025 12:53:35 -0400 Subject: [PATCH 18/91] docs: document some missing features (#4281) --- leptos/src/lib.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/leptos/src/lib.rs b/leptos/src/lib.rs index ad70ca17a2..d287df6f75 100644 --- a/leptos/src/lib.rs +++ b/leptos/src/lib.rs @@ -85,12 +85,22 @@ //! # Feature Flags //! //! - **`nightly`**: On `nightly` Rust, enables the function-call syntax for signal getters and setters. +//! Also enables some experimental optimizations that improve the handling of static strings and +//! the performance of the `template! {}` macro. //! - **`csr`** Client-side rendering: Generate DOM nodes in the browser. //! - **`ssr`** Server-side rendering: Generate an HTML string (typically on the server). +//! - **`islands`** Activates “islands mode,” in which components are not made interactive on the +//! client unless they use the `#[island]` macro. //! - **`hydrate`** Hydration: use this to add interactivity to an SSRed Leptos app. -//! - **`rkyv`** In SSR/hydrate mode, uses [`rkyv`](https://docs.rs/rkyv/latest/rkyv/) to serialize resources and send them -//! from the server to the client. +//! - **`nonce`** Adds support for nonces to be added as part of a Content Security Policy. +//! - **`rkyv`** In SSR/hydrate mode, enables using [`rkyv`](https://docs.rs/rkyv/latest/rkyv/) to serialize resources. //! - **`tracing`** Adds support for [`tracing`](https://docs.rs/tracing/latest/tracing/). +//! - **`trace-component-props`** Adds `tracing` support for component props. +//! - **`delegation`** Uses event delegation rather than the browser’s native event handling +//! system. (This improves the performance of creating large numbers of elements simultaneously, +//! in exchange for occasional edge cases in which events behave differently from native browser +//! events.) +//! - **`rustls`** Use `rustls` for server functions. //! //! **Important Note:** You must enable one of `csr`, `hydrate`, or `ssr` to tell Leptos //! which mode your app is operating in. You should only enable one of these per build target, From 16709841cb1d9b8a95eab4ca916bd45510eacbca Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Fri, 12 Sep 2025 17:20:52 -0400 Subject: [PATCH 19/91] fix: prevent double-rebuild and correctly navigate multiple times to same lazy route (closes #4285) --- router/src/nested_router.rs | 96 +++++++++++++++++++++---------------- 1 file changed, 54 insertions(+), 42 deletions(-) diff --git a/router/src/nested_router.rs b/router/src/nested_router.rs index 1c7bcb2e13..21553ad642 100644 --- a/router/src/nested_router.rs +++ b/router/src/nested_router.rs @@ -19,7 +19,7 @@ use leptos::{ attr::any_attribute::AnyAttribute, component, oco::Oco, - prelude::{ArcStoredValue, WriteValue}, + prelude::{ArcStoredValue, Write, WriteValue}, }; use or_poisoned::OrPoisoned; use reactive_graph::{ @@ -114,12 +114,14 @@ where let matched_view = match new_match { None => EitherOf3::B(fallback()), Some(route) => { - route.build_nested_route( - &url, - base, - &mut loaders, - &mut outlets, - ); + outer_owner.with(|| { + route.build_nested_route( + &url, + base, + &mut loaders, + &mut outlets, + ) + }); drop(url); EitherOf3::C(top_level_outlet(&outlets, &outer_owner)) @@ -165,7 +167,7 @@ where let new_match = self.routes.match_route(url_snapshot.path()); - state.current_url.set(url_snapshot); + *state.current_url.write_untracked() = url_snapshot; match new_match { None => { @@ -183,16 +185,18 @@ where let mut preloaders = Vec::new(); let mut full_loaders = Vec::new(); - let different_level = route.rebuild_nested_route( - &self.current_url.read_untracked(), - self.base, - &mut 0, - &mut preloaders, - &mut full_loaders, - &mut state.outlets, - self.set_is_routing.is_some(), - 0, - ); + let different_level = self.outer_owner.with(|| { + route.rebuild_nested_route( + &self.current_url.read_untracked(), + self.base, + &mut 0, + &mut preloaders, + &mut full_loaders, + &mut state.outlets, + self.set_is_routing.is_some(), + 0, + ) + }); let (abort_handle, abort_registration) = AbortHandle::new_pair(); @@ -364,12 +368,14 @@ where None => Either::Left(fallback()), Some(route) => { let mut loaders = Vec::new(); - route.build_nested_route( - ¤t_url, - base, - &mut loaders, - &mut outlets, - ); + outer_owner.with(|| { + route.build_nested_route( + ¤t_url, + base, + &mut loaders, + &mut outlets, + ) + }); // outlets will not send their views if the loaders are never polled // the loaders are async so that they can lazy-load routes in the browser, @@ -417,12 +423,14 @@ where None => Either::Left(fallback()), Some(route) => { let mut loaders = Vec::new(); - route.build_nested_route( - ¤t_url, - base, - &mut loaders, - &mut outlets, - ); + outer_owner.with(|| { + route.build_nested_route( + ¤t_url, + base, + &mut loaders, + &mut outlets, + ) + }); // outlets will not send their views if the loaders are never polled // the loaders are async so that they can lazy-load routes in the browser, @@ -470,12 +478,14 @@ where match new_match { None => EitherOf3::B(fallback()), Some(route) => { - route.build_nested_route( - &url, - base, - &mut loaders, - &mut outlets, - ); + outer_owner.with(|| { + route.build_nested_route( + &url, + base, + &mut loaders, + &mut outlets, + ) + }); drop(url); join_all(mem::take(&mut loaders)).now_or_never().expect( @@ -525,12 +535,14 @@ where match new_match { None => EitherOf3::B(fallback()), Some(route) => { - route.build_nested_route( - &url, - base, - &mut loaders, - &mut outlets, - ); + outer_owner.with(|| { + route.build_nested_route( + &url, + base, + &mut loaders, + &mut outlets, + ); + }); drop(url); join_all(mem::take(&mut loaders)).await; From 4643aa7bb01b5b3fdeff38acc705a5c702e54d52 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Fri, 12 Sep 2025 17:59:14 -0400 Subject: [PATCH 20/91] Revert "fix: prevent double-rebuild and correctly navigate multiple times to same lazy route (closes #4285)" This reverts commit d37512bebd3876c1f455bbd5f088115dc60c9d37. --- router/src/nested_router.rs | 96 ++++++++++++++++--------------------- 1 file changed, 42 insertions(+), 54 deletions(-) diff --git a/router/src/nested_router.rs b/router/src/nested_router.rs index 21553ad642..1c7bcb2e13 100644 --- a/router/src/nested_router.rs +++ b/router/src/nested_router.rs @@ -19,7 +19,7 @@ use leptos::{ attr::any_attribute::AnyAttribute, component, oco::Oco, - prelude::{ArcStoredValue, Write, WriteValue}, + prelude::{ArcStoredValue, WriteValue}, }; use or_poisoned::OrPoisoned; use reactive_graph::{ @@ -114,14 +114,12 @@ where let matched_view = match new_match { None => EitherOf3::B(fallback()), Some(route) => { - outer_owner.with(|| { - route.build_nested_route( - &url, - base, - &mut loaders, - &mut outlets, - ) - }); + route.build_nested_route( + &url, + base, + &mut loaders, + &mut outlets, + ); drop(url); EitherOf3::C(top_level_outlet(&outlets, &outer_owner)) @@ -167,7 +165,7 @@ where let new_match = self.routes.match_route(url_snapshot.path()); - *state.current_url.write_untracked() = url_snapshot; + state.current_url.set(url_snapshot); match new_match { None => { @@ -185,18 +183,16 @@ where let mut preloaders = Vec::new(); let mut full_loaders = Vec::new(); - let different_level = self.outer_owner.with(|| { - route.rebuild_nested_route( - &self.current_url.read_untracked(), - self.base, - &mut 0, - &mut preloaders, - &mut full_loaders, - &mut state.outlets, - self.set_is_routing.is_some(), - 0, - ) - }); + let different_level = route.rebuild_nested_route( + &self.current_url.read_untracked(), + self.base, + &mut 0, + &mut preloaders, + &mut full_loaders, + &mut state.outlets, + self.set_is_routing.is_some(), + 0, + ); let (abort_handle, abort_registration) = AbortHandle::new_pair(); @@ -368,14 +364,12 @@ where None => Either::Left(fallback()), Some(route) => { let mut loaders = Vec::new(); - outer_owner.with(|| { - route.build_nested_route( - ¤t_url, - base, - &mut loaders, - &mut outlets, - ) - }); + route.build_nested_route( + ¤t_url, + base, + &mut loaders, + &mut outlets, + ); // outlets will not send their views if the loaders are never polled // the loaders are async so that they can lazy-load routes in the browser, @@ -423,14 +417,12 @@ where None => Either::Left(fallback()), Some(route) => { let mut loaders = Vec::new(); - outer_owner.with(|| { - route.build_nested_route( - ¤t_url, - base, - &mut loaders, - &mut outlets, - ) - }); + route.build_nested_route( + ¤t_url, + base, + &mut loaders, + &mut outlets, + ); // outlets will not send their views if the loaders are never polled // the loaders are async so that they can lazy-load routes in the browser, @@ -478,14 +470,12 @@ where match new_match { None => EitherOf3::B(fallback()), Some(route) => { - outer_owner.with(|| { - route.build_nested_route( - &url, - base, - &mut loaders, - &mut outlets, - ) - }); + route.build_nested_route( + &url, + base, + &mut loaders, + &mut outlets, + ); drop(url); join_all(mem::take(&mut loaders)).now_or_never().expect( @@ -535,14 +525,12 @@ where match new_match { None => EitherOf3::B(fallback()), Some(route) => { - outer_owner.with(|| { - route.build_nested_route( - &url, - base, - &mut loaders, - &mut outlets, - ); - }); + route.build_nested_route( + &url, + base, + &mut loaders, + &mut outlets, + ); drop(url); join_all(mem::take(&mut loaders)).await; From 616e45780813cac9bfe496294e962193e330ff2b Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Fri, 12 Sep 2025 18:00:13 -0400 Subject: [PATCH 21/91] fix: prevent infinite rebuild loop --- router/src/nested_router.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/router/src/nested_router.rs b/router/src/nested_router.rs index 1c7bcb2e13..4ca14ad4f9 100644 --- a/router/src/nested_router.rs +++ b/router/src/nested_router.rs @@ -26,7 +26,7 @@ use reactive_graph::{ computed::{ArcMemo, ScopedFuture}, owner::{provide_context, use_context, Owner}, signal::{ArcRwSignal, ArcTrigger}, - traits::{Get, GetUntracked, Notify, ReadUntracked, Set, Track}, + traits::{Get, GetUntracked, Notify, ReadUntracked, Set, Track, Write}, transition::AsyncTransition, wrappers::write::SignalSetter, }; @@ -165,7 +165,7 @@ where let new_match = self.routes.match_route(url_snapshot.path()); - state.current_url.set(url_snapshot); + *state.current_url.write_untracked() = url_snapshot; match new_match { None => { From 51b0cc9e837cbdaf12f87304c6af22174076d067 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Fri, 12 Sep 2025 18:51:46 -0400 Subject: [PATCH 22/91] fix: create individual owners for each preload --- router/src/nested_router.rs | 70 +++++++++++++++++++++++++++++++------ 1 file changed, 60 insertions(+), 10 deletions(-) diff --git a/router/src/nested_router.rs b/router/src/nested_router.rs index 4ca14ad4f9..76ee6720b2 100644 --- a/router/src/nested_router.rs +++ b/router/src/nested_router.rs @@ -119,6 +119,7 @@ where base, &mut loaders, &mut outlets, + &outer_owner, ); drop(url); @@ -159,6 +160,7 @@ where } return; } + // since the path didn't match, we'll update the retained path for future diffing state.path.clear(); state.path.push_str(url_snapshot.path()); @@ -192,6 +194,7 @@ where &mut state.outlets, self.set_is_routing.is_some(), 0, + &self.outer_owner, ); let (abort_handle, abort_registration) = @@ -369,6 +372,7 @@ where base, &mut loaders, &mut outlets, + &outer_owner, ); // outlets will not send their views if the loaders are never polled @@ -422,6 +426,7 @@ where base, &mut loaders, &mut outlets, + &outer_owner, ); // outlets will not send their views if the loaders are never polled @@ -475,6 +480,7 @@ where base, &mut loaders, &mut outlets, + &outer_owner, ); drop(url); @@ -530,6 +536,7 @@ where base, &mut loaders, &mut outlets, + &outer_owner, ); drop(url); @@ -566,6 +573,7 @@ pub(crate) struct RouteContext { base: Option<Oco<'static, str>>, view_fn: Arc<Mutex<OutletViewFn>>, owner: Arc<Mutex<Option<Owner>>>, + preload_owner: Owner, child: ChildRoute, } @@ -597,6 +605,7 @@ impl Clone for RouteContext { view_fn: Arc::clone(&self.view_fn), owner: Arc::clone(&self.owner), child: self.child.clone(), + preload_owner: self.preload_owner.clone(), } } } @@ -608,6 +617,7 @@ trait AddNestedRoute { base: Option<Oco<'static, str>>, loaders: &mut Vec<Pin<Box<dyn Future<Output = ArcTrigger>>>>, outlets: &mut Vec<RouteContext>, + outer_owner: &Owner, ); #[allow(clippy::too_many_arguments)] @@ -621,6 +631,7 @@ trait AddNestedRoute { outlets: &mut Vec<RouteContext>, set_is_routing: bool, level: u8, + outer_owner: &Owner, ) -> u8; } @@ -634,6 +645,7 @@ where base: Option<Oco<'static, str>>, loaders: &mut Vec<Pin<Box<dyn Future<Output = ArcTrigger>>>>, outlets: &mut Vec<RouteContext>, + outer_owner: &Owner, ) { let orig_url = url; @@ -701,6 +713,7 @@ where base: base.clone(), child: ChildRoute(Arc::new(Mutex::new(None))), owner: Arc::new(Mutex::new(None)), + preload_owner: outer_owner.child(), }; if !outlets.is_empty() { let prev_index = outlets.len().saturating_sub(1); @@ -725,7 +738,10 @@ where provide_context(params.clone()); provide_context(url.clone()); provide_context(matched.clone()); - view.preload().await; + outlet + .preload_owner + .with(|| ScopedFuture::new(view.preload())) + .await; let child = outlet.child.clone(); *view_fn.lock().or_poisoned() = Box::new(move |owner_where_used| { @@ -772,7 +788,13 @@ where // this is important because to build the view, we need access to the outlet // and the outlet will be returned from building this child if let Some(child) = child { - child.build_nested_route(orig_url, base, loaders, outlets); + child.build_nested_route( + orig_url, + base, + loaders, + outlets, + outer_owner, + ); } } @@ -787,6 +809,7 @@ where outlets: &mut Vec<RouteContext>, set_is_routing: bool, level: u8, + outer_owner: &Owner, ) -> u8 { let (parent_params, parent_matches): (Vec<_>, Vec<_>) = outlets .iter() @@ -803,7 +826,13 @@ where match current { // if there's nothing currently in the routes at this point, build from here None => { - self.build_nested_route(url, base, preloaders, outlets); + self.build_nested_route( + url, + base, + preloaders, + outlets, + outer_owner, + ); level } Some(current) => { @@ -843,6 +872,10 @@ where &mut current.matched, ArcRwSignal::new(new_match), ); + let old_preload_owner = mem::replace( + &mut current.preload_owner, + outer_owner.child(), + ); let matched_including_parents = { ArcMemo::new({ let matched = current.matched.clone(); @@ -885,11 +918,21 @@ where let child = outlet.child.clone(); async move { let child = child.clone(); - if set_is_routing { - AsyncTransition::run(|| view.preload()).await; - } else { - view.preload().await; - } + outlet + .preload_owner + .with(|| { + ScopedFuture::new(async { + if set_is_routing { + AsyncTransition::run(|| { + view.preload() + }) + .await; + } else { + view.preload().await; + } + }) + }) + .await; *view_fn.lock().or_poisoned() = Box::new(move |owner_where_used| { let prev_owner = route_owner @@ -938,6 +981,7 @@ where drop(old_params); drop(old_url); drop(old_matched); + drop(old_preload_owner); trigger } }))); @@ -948,8 +992,13 @@ where // if this children has matches, then rebuild the lower section of the tree if let Some(child) = child { - child - .build_nested_route(url, base, preloaders, outlets); + child.build_nested_route( + url, + base, + preloaders, + outlets, + outer_owner, + ); } else { *outlets[*items].child.0.lock().or_poisoned() = None; } @@ -973,6 +1022,7 @@ where outlets, set_is_routing, level + 1, + outer_owner, ) } else { *current.child.0.lock().or_poisoned() = None; From 869a3c48f6454c58ce0c2792fef2e22e58dc06e4 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Mon, 15 Sep 2025 18:54:11 -0400 Subject: [PATCH 23/91] during SSR, don't dispose of preload owners until whole request is done --- router/src/nested_router.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/router/src/nested_router.rs b/router/src/nested_router.rs index 76ee6720b2..8e3d2aa3b8 100644 --- a/router/src/nested_router.rs +++ b/router/src/nested_router.rs @@ -429,6 +429,13 @@ where &outer_owner, ); + let preload_owners = outlets + .iter() + .map(|o| o.preload_owner.clone()) + .collect::<Vec<_>>(); + outer_owner + .with(|| Owner::on_cleanup(move || drop(preload_owners))); + // outlets will not send their views if the loaders are never polled // the loaders are async so that they can lazy-load routes in the browser, // but they should always be synchronously available on the server From 39e1fc27b448b24b3c50f600dc33e917923aa37d Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Mon, 15 Sep 2025 19:46:52 -0400 Subject: [PATCH 24/91] fix: provide correct URL/query/params to preloaders (closes #4296) --- router/src/nested_router.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/router/src/nested_router.rs b/router/src/nested_router.rs index 8e3d2aa3b8..4311ab5c58 100644 --- a/router/src/nested_router.rs +++ b/router/src/nested_router.rs @@ -747,7 +747,12 @@ where provide_context(matched.clone()); outlet .preload_owner - .with(|| ScopedFuture::new(view.preload())) + .with(|| { + provide_context(params.clone()); + provide_context(url.clone()); + provide_context(matched.clone()); + ScopedFuture::new(view.preload()) + }) .await; let child = outlet.child.clone(); *view_fn.lock().or_poisoned() = @@ -928,6 +933,11 @@ where outlet .preload_owner .with(|| { + provide_context( + params_including_parents.clone(), + ); + provide_context(url.clone()); + provide_context(matched.clone()); ScopedFuture::new(async { if set_is_routing { AsyncTransition::run(|| { From 9fe8b50b8767e8c5d1a9022bd4272913c0e8a42f Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Mon, 15 Sep 2025 21:05:12 -0400 Subject: [PATCH 25/91] test: add regression test for #4285 --- .../e2e/features/issue_4285.feature | 9 ++++ examples/regression/src/app.rs | 8 ++- examples/regression/src/issue_4285.rs | 49 +++++++++++++++++++ examples/regression/src/lib.rs | 1 + 4 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 examples/regression/e2e/features/issue_4285.feature create mode 100644 examples/regression/src/issue_4285.rs diff --git a/examples/regression/e2e/features/issue_4285.feature b/examples/regression/e2e/features/issue_4285.feature new file mode 100644 index 0000000000..a8d593f48e --- /dev/null +++ b/examples/regression/e2e/features/issue_4285.feature @@ -0,0 +1,9 @@ +@check_issue_4005 +Feature: Check that issue 4005 does not reappear + + Scenario: Navigating several times to same lazy route does not cause issues. + Given I see the app + And I can access regression test 4285 + And I can access regression test 4285 + And I can access regression test 4285 + Then I see the result is the string 42 diff --git a/examples/regression/src/app.rs b/examples/regression/src/app.rs index ed85d4ea4e..6e34905a0c 100644 --- a/examples/regression/src/app.rs +++ b/examples/regression/src/app.rs @@ -1,6 +1,6 @@ use crate::{ issue_4005::Routes4005, issue_4088::Routes4088, issue_4217::Routes4217, - pr_4015::Routes4015, pr_4091::Routes4091, + issue_4285::Routes4285, pr_4015::Routes4015, pr_4091::Routes4091, }; use leptos::prelude::*; use leptos_meta::{MetaTags, *}; @@ -31,9 +31,11 @@ pub fn shell(options: LeptosOptions) -> impl IntoView { pub fn App() -> impl IntoView { provide_meta_context(); let fallback = || view! { "Page not found." }.into_view(); + let (_, set_is_routing) = signal(false); + view! { <Stylesheet id="leptos" href="/pkg/regression.css"/> - <Router> + <Router set_is_routing> <main> <Routes fallback> <Route path=path!("") view=HomePage/> @@ -42,6 +44,7 @@ pub fn App() -> impl IntoView { <Routes4088/> <Routes4217/> <Routes4005/> + <Routes4285/> </Routes> </main> </Router> @@ -66,6 +69,7 @@ fn HomePage() -> impl IntoView { <li><a href="/4088/">"4088"</a></li> <li><a href="/4217/">"4217"</a></li> <li><a href="/4005/">"4005"</a></li> + <li><a href="/4285/">"4285"</a></li> </ul> </nav> } diff --git a/examples/regression/src/issue_4285.rs b/examples/regression/src/issue_4285.rs new file mode 100644 index 0000000000..d51f35247c --- /dev/null +++ b/examples/regression/src/issue_4285.rs @@ -0,0 +1,49 @@ +use leptos::prelude::*; +use leptos_router::LazyRoute; +#[allow(unused_imports)] +use leptos_router::{ + components::Route, path, Lazy, MatchNestedRoutes, NavigateOptions, +}; + +#[component] +pub fn Routes4285() -> impl MatchNestedRoutes + Clone { + view! { + <Route path=path!("4285") view={Lazy::<Issue4285>::new()}/> + } + .into_inner() +} + +struct Issue4285 { + data: Resource<Result<i32, ServerFnError>>, +} + +impl LazyRoute for Issue4285 { + fn data() -> Self { + Self { + data: Resource::new(|| (), |_| slow_call()), + } + } + + async fn view(this: Self) -> AnyView { + let Issue4285 { data } = this; + view! { + <Suspense> + {move || { + Suspend::new(async move { + let data = data.await; + view! { + <p id="result">{data}</p> + } + }) + }} + </Suspense> + } + .into_any() + } +} + +#[server] +async fn slow_call() -> Result<i32, ServerFnError> { + tokio::time::sleep(std::time::Duration::from_millis(250)).await; + Ok(42) +} diff --git a/examples/regression/src/lib.rs b/examples/regression/src/lib.rs index 9fc8969389..44afab978e 100644 --- a/examples/regression/src/lib.rs +++ b/examples/regression/src/lib.rs @@ -2,6 +2,7 @@ pub mod app; mod issue_4005; mod issue_4088; mod issue_4217; +mod issue_4285; mod pr_4015; mod pr_4091; From d55561f84d1442137d28a7d54fe70de42944690e Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Tue, 16 Sep 2025 16:07:00 -0400 Subject: [PATCH 26/91] chore: correct name for test --- examples/regression/e2e/features/issue_4285.feature | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/regression/e2e/features/issue_4285.feature b/examples/regression/e2e/features/issue_4285.feature index a8d593f48e..0bea5e02fd 100644 --- a/examples/regression/e2e/features/issue_4285.feature +++ b/examples/regression/e2e/features/issue_4285.feature @@ -1,5 +1,5 @@ -@check_issue_4005 -Feature: Check that issue 4005 does not reappear +@check_issue_4285 +Feature: Check that issue 4285 does not reappear Scenario: Navigating several times to same lazy route does not cause issues. Given I see the app From 205416b3bbf79a2be872a1e968f2b55b702f943f Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Tue, 16 Sep 2025 16:22:42 -0400 Subject: [PATCH 27/91] test: add regression test for #4296 --- .../e2e/features/issue_4296.feature | 18 ++++++++++ examples/regression/src/app.rs | 5 ++- examples/regression/src/issue_4296.rs | 36 +++++++++++++++++++ examples/regression/src/lib.rs | 1 + 4 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 examples/regression/e2e/features/issue_4296.feature create mode 100644 examples/regression/src/issue_4296.rs diff --git a/examples/regression/e2e/features/issue_4296.feature b/examples/regression/e2e/features/issue_4296.feature new file mode 100644 index 0000000000..9be920a880 --- /dev/null +++ b/examples/regression/e2e/features/issue_4296.feature @@ -0,0 +1,18 @@ +@check_issue_4296 +Feature: Check that issue 4296 does not reappear + + Scenario: Query param signals created in LazyRoute::data() are reactive in ::view(). + Given I see the app + And I can access regression test 4296 + Then I see the result is the string None + When I select the link abc + Then I see the result is the string Some("abc") + When I select the link def + Then I see the result is the string Some("def") + + Scenario: Loading page with query signal works as well. + Given I see the app + And I can access regression test 4296 + When I select the link abc + When I reload the page + Then I see the result is the string Some("abc") diff --git a/examples/regression/src/app.rs b/examples/regression/src/app.rs index 6e34905a0c..134a92d2cc 100644 --- a/examples/regression/src/app.rs +++ b/examples/regression/src/app.rs @@ -1,6 +1,7 @@ use crate::{ issue_4005::Routes4005, issue_4088::Routes4088, issue_4217::Routes4217, - issue_4285::Routes4285, pr_4015::Routes4015, pr_4091::Routes4091, + issue_4285::Routes4285, issue_4296::Routes4296, pr_4015::Routes4015, + pr_4091::Routes4091, }; use leptos::prelude::*; use leptos_meta::{MetaTags, *}; @@ -45,6 +46,7 @@ pub fn App() -> impl IntoView { <Routes4217/> <Routes4005/> <Routes4285/> + <Routes4296/> </Routes> </main> </Router> @@ -70,6 +72,7 @@ fn HomePage() -> impl IntoView { <li><a href="/4217/">"4217"</a></li> <li><a href="/4005/">"4005"</a></li> <li><a href="/4285/">"4285"</a></li> + <li><a href="/4296/">"4296"</a></li> </ul> </nav> } diff --git a/examples/regression/src/issue_4296.rs b/examples/regression/src/issue_4296.rs new file mode 100644 index 0000000000..338ef15157 --- /dev/null +++ b/examples/regression/src/issue_4296.rs @@ -0,0 +1,36 @@ +use leptos::prelude::*; +#[allow(unused_imports)] +use leptos_router::{ + components::Route, path, Lazy, MatchNestedRoutes, NavigateOptions, +}; +use leptos_router::{hooks::use_query_map, LazyRoute}; + +#[component] +pub fn Routes4296() -> impl MatchNestedRoutes + Clone { + view! { + <Route path=path!("4296") view={Lazy::<Issue4296>::new()}/> + } + .into_inner() +} + +struct Issue4296 { + query: Signal<Option<String>>, +} + +impl LazyRoute for Issue4296 { + fn data() -> Self { + let query = use_query_map(); + let query = Signal::derive(move || query.read().get("q")); + Self { query } + } + + async fn view(this: Self) -> AnyView { + let Issue4296 { query } = this; + view! { + <a href="?q=abc">"abc"</a> + <a href="?q=def">"def"</a> + <p id="result">{move || format!("{:?}", query.get())}</p> + } + .into_any() + } +} diff --git a/examples/regression/src/lib.rs b/examples/regression/src/lib.rs index 44afab978e..a326d4e4df 100644 --- a/examples/regression/src/lib.rs +++ b/examples/regression/src/lib.rs @@ -3,6 +3,7 @@ mod issue_4005; mod issue_4088; mod issue_4217; mod issue_4285; +mod issue_4296; mod pr_4015; mod pr_4091; From 04985dbaf4c4ef2de84f0c7830dbfc4d0a79821e Mon Sep 17 00:00:00 2001 From: Adam Doyle <24621027+adoyle0@users.noreply.github.com> Date: Thu, 18 Sep 2025 09:05:27 -0400 Subject: [PATCH 28/91] chore: add referrerpolicy attribute to a element (#4299) --- tachys/src/html/element/elements.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tachys/src/html/element/elements.rs b/tachys/src/html/element/elements.rs index cdd5430927..17328e0be3 100644 --- a/tachys/src/html/element/elements.rs +++ b/tachys/src/html/element/elements.rs @@ -227,7 +227,7 @@ html_self_closing_elements! { html_elements! { /// The `<a>` HTML element (or anchor element), with its href attribute, creates a hyperlink to web pages, files, email addresses, locations in the same page, or anything else a URL can address. - a HtmlAnchorElement [download, href, hreflang, ping, rel, target, r#type ] true, + a HtmlAnchorElement [download, href, hreflang, ping, referrerpolicy, rel, target, r#type ] true, /// The `<abbr>` HTML element represents an abbreviation or acronym; the optional title attribute can provide an expansion or description for the abbreviation. If present, title must contain this full description and nothing else. abbr HtmlElement [] true, /// The `<address>` HTML element indicates that the enclosed HTML provides contact information for a person or people, or for an organization. From c3b408ce2da0522732897dff1853ed314c7e58f0 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Thu, 18 Sep 2025 09:09:36 -0400 Subject: [PATCH 29/91] fix: support const generic static strs on nightly versions with conflicting feature names (closes #4300) (#4301) --- tachys/src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tachys/src/lib.rs b/tachys/src/lib.rs index 00aa1e02c2..7db72bd26f 100644 --- a/tachys/src/lib.rs +++ b/tachys/src/lib.rs @@ -10,6 +10,9 @@ all(feature = "nightly", rustc_nightly), feature(unsized_const_params) )] +// support for const generic &'static str has now moved back and forth between +// these two features a couple times; we'll just enable both +#![cfg_attr(all(feature = "nightly", rustc_nightly), feature(adt_const_params))] #![deny(missing_docs)] /// Commonly-used traits. From d60c19ccda3263b55dccbfd517ea8e63b8a41505 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Thu, 18 Sep 2025 15:49:46 -0400 Subject: [PATCH 30/91] `leptos` v0.8.9 --- Cargo.lock | 12 ++++++------ Cargo.toml | 12 ++++++------ leptos/Cargo.toml | 2 +- reactive_graph/Cargo.toml | 2 +- router/Cargo.toml | 2 +- server_fn/Cargo.toml | 2 +- tachys/Cargo.toml | 2 +- wasm_split_macros/Cargo.toml | 2 +- 8 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6c151db55b..bd8f7cb3f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1796,7 +1796,7 @@ checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" [[package]] name = "leptos" -version = "0.8.8" +version = "0.8.9" dependencies = [ "any_spawner", "base64", @@ -2004,7 +2004,7 @@ dependencies = [ [[package]] name = "leptos_router" -version = "0.8.6" +version = "0.8.7" dependencies = [ "any_spawner", "either_of", @@ -2754,7 +2754,7 @@ dependencies = [ [[package]] name = "reactive_graph" -version = "0.2.6" +version = "0.2.7" dependencies = [ "any_spawner", "async-lock", @@ -3291,7 +3291,7 @@ dependencies = [ [[package]] name = "server_fn" -version = "0.8.6" +version = "0.8.7" dependencies = [ "actix-web", "actix-ws", @@ -3581,7 +3581,7 @@ dependencies = [ [[package]] name = "tachys" -version = "0.2.7" +version = "0.2.8" dependencies = [ "any_spawner", "async-trait", @@ -4367,7 +4367,7 @@ dependencies = [ [[package]] name = "wasm_split_macros" -version = "0.1.2" +version = "0.1.3" dependencies = [ "base16", "digest", diff --git a/Cargo.toml b/Cargo.toml index de6df7a630..07a702fed0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,28 +50,28 @@ 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.8" } +leptos = { path = "./leptos", version = "0.8.9" } leptos_config = { path = "./leptos_config", version = "0.8.7" } leptos_dom = { path = "./leptos_dom", version = "0.8.6" } leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.5" } leptos_integration_utils = { path = "./integrations/utils", version = "0.8.5" } leptos_macro = { path = "./leptos_macro", version = "0.8.8" } -leptos_router = { path = "./router", version = "0.8.6" } +leptos_router = { path = "./router", version = "0.8.7" } leptos_router_macro = { path = "./router_macro", version = "0.8.5" } leptos_server = { path = "./leptos_server", version = "0.8.5" } 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.6" } +reactive_graph = { path = "./reactive_graph", version = "0.2.7" } reactive_stores = { path = "./reactive_stores", version = "0.2.5" } reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.2.6" } -server_fn = { path = "./server_fn", version = "0.8.6" } +server_fn = { path = "./server_fn", version = "0.8.7" } server_fn_macro = { path = "./server_fn_macro", version = "0.8.7" } server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.5" } -tachys = { path = "./tachys", version = "0.2.7" } +tachys = { path = "./tachys", version = "0.2.8" } wasm_split_helpers = { path = "./wasm_split", version = "0.1.2" } -wasm_split_macros = { path = "./wasm_split_macros", version = "0.1.2" } +wasm_split_macros = { path = "./wasm_split_macros", version = "0.1.3" } # members deps async-once-cell = { default-features = false, version = "0.5.3" } diff --git a/leptos/Cargo.toml b/leptos/Cargo.toml index cdf6bd4325..1661e25b6b 100644 --- a/leptos/Cargo.toml +++ b/leptos/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "leptos" -version = "0.8.8" +version = "0.8.9" authors = ["Greg Johnston"] license = "MIT" repository = "https://github.com/leptos-rs/leptos" diff --git a/reactive_graph/Cargo.toml b/reactive_graph/Cargo.toml index e61a58c9c3..d63ca8aa5f 100644 --- a/reactive_graph/Cargo.toml +++ b/reactive_graph/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "reactive_graph" -version = "0.2.6" +version = "0.2.7" authors = ["Greg Johnston"] license = "MIT" readme = "../README.md" diff --git a/router/Cargo.toml b/router/Cargo.toml index 4cf7a47a2e..22a20044fa 100644 --- a/router/Cargo.toml +++ b/router/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "leptos_router" -version = "0.8.6" +version = "0.8.7" authors = ["Greg Johnston", "Ben Wishovich"] license = "MIT" readme = "../README.md" diff --git a/server_fn/Cargo.toml b/server_fn/Cargo.toml index c7f57ee6c3..6f0bcfb4a0 100644 --- a/server_fn/Cargo.toml +++ b/server_fn/Cargo.toml @@ -5,7 +5,7 @@ license = "MIT" repository = "https://github.com/leptos-rs/leptos" description = "RPC for any web framework." readme = "../README.md" -version = "0.8.6" +version = "0.8.7" rust-version.workspace = true edition.workspace = true diff --git a/tachys/Cargo.toml b/tachys/Cargo.toml index 7199b5d67a..e4e8003c76 100644 --- a/tachys/Cargo.toml +++ b/tachys/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tachys" -version = "0.2.7" +version = "0.2.8" authors = ["Greg Johnston"] license = "MIT" readme = "../README.md" diff --git a/wasm_split_macros/Cargo.toml b/wasm_split_macros/Cargo.toml index 09d39e3071..4c9c4de14f 100644 --- a/wasm_split_macros/Cargo.toml +++ b/wasm_split_macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "wasm_split_macros" -version = "0.1.2" +version = "0.1.3" authors = ["Greg Johnston"] license = "MIT" readme = "README.md" From a28f7ef161977cf27019adca06ba53b337f86ae1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=86gir=20=C3=96rn=20S=C3=ADmonarson?= <agirorn@gmail.com> Date: Fri, 19 Sep 2025 15:35:51 +0000 Subject: [PATCH 31/91] Locking dependencies in cargo-leptos install example (#4295) This limits dependencies errors on install --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 78dc9d13cf..91a3a8f502 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ Here are some resources for learning more about Leptos: [`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos) is a build tool that's designed to make it easy to build apps that run on both the client and the server, with seamless integration. The best way to get started with a real Leptos project right now is to use `cargo-leptos` and our starter templates for [Actix](https://github.com/leptos-rs/start) or [Axum](https://github.com/leptos-rs/start-axum). ```bash -cargo install cargo-leptos +cargo install cargo-leptos --locked cargo leptos new --git https://github.com/leptos-rs/start-axum cd [your project name] cargo leptos watch From 7b6c8af85069ce91f645be08f1ba4fb61c6a3ef4 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Sat, 20 Sep 2025 11:40:28 -0400 Subject: [PATCH 32/91] feat: minimal support for `subsecond` and an example (#4307) --- Cargo.lock | 570 ++++++++++++------ Cargo.toml | 3 + examples/subsecond_hot_patch/.gitignore | 7 + examples/subsecond_hot_patch/Cargo.toml | 13 + examples/subsecond_hot_patch/Dioxus.toml | 21 + examples/subsecond_hot_patch/Makefile.toml | 1 + examples/subsecond_hot_patch/README.md | 31 + .../subsecond_hot_patch/assets/favicon.ico | Bin 0 -> 132770 bytes .../subsecond_hot_patch/assets/header.svg | 20 + examples/subsecond_hot_patch/assets/main.css | 46 ++ examples/subsecond_hot_patch/src/main.rs | 44 ++ leptos/Cargo.toml | 13 + leptos/src/lib.rs | 5 +- leptos/src/subsecond.rs | 62 ++ leptos_dom/src/helpers.rs | 2 +- reactive_graph/Cargo.toml | 2 + reactive_graph/src/effect/render_effect.rs | 118 +++- 17 files changed, 776 insertions(+), 182 deletions(-) create mode 100644 examples/subsecond_hot_patch/.gitignore create mode 100644 examples/subsecond_hot_patch/Cargo.toml create mode 100644 examples/subsecond_hot_patch/Dioxus.toml create mode 100644 examples/subsecond_hot_patch/Makefile.toml create mode 100644 examples/subsecond_hot_patch/README.md create mode 100644 examples/subsecond_hot_patch/assets/favicon.ico create mode 100644 examples/subsecond_hot_patch/assets/header.svg create mode 100644 examples/subsecond_hot_patch/assets/main.css create mode 100644 examples/subsecond_hot_patch/src/main.rs create mode 100644 leptos/src/subsecond.rs diff --git a/Cargo.lock b/Cargo.lock index bd8f7cb3f3..9d183b0520 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,9 +21,9 @@ dependencies = [ [[package]] name = "actix-files" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0773d59061dedb49a8aed04c67291b9d8cf2fe0b60130a381aab53c6dd86e9be" +checksum = "6c0d87f10d70e2948ad40e8edea79c8e77c6c66e0250a4c1f09b690465199576" dependencies = [ "actix-http", "actix-service", @@ -31,7 +31,7 @@ dependencies = [ "actix-web", "bitflags", "bytes", - "derive_more 0.99.20", + "derive_more", "futures-core", "http-range", "log", @@ -57,7 +57,7 @@ dependencies = [ "brotli", "bytes", "bytestring", - "derive_more 2.0.1", + "derive_more", "encoding_rs", "flate2", "foldhash", @@ -108,9 +108,9 @@ dependencies = [ [[package]] name = "actix-rt" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" +checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63" dependencies = [ "futures-core", "tokio", @@ -172,7 +172,7 @@ dependencies = [ "bytestring", "cfg-if", "cookie", - "derive_more 2.0.1", + "derive_more", "encoding_rs", "foldhash", "futures-core", @@ -285,9 +285,9 @@ checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" [[package]] name = "async-executor" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb812ffb58524bdd10860d7d974e2f01cc0950c2438a74ee5ec2e2280c6c4ffa" +checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" dependencies = [ "async-task", "concurrent-queue", @@ -491,9 +491,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.9.3" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" [[package]] name = "block-buffer" @@ -568,25 +568,26 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "bytestring" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e465647ae23b2823b0753f50decb2d5a86d2bb2cac04788fafd1f80e45378e5f" +checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289" dependencies = [ "bytes", ] [[package]] name = "camino" -version = "1.1.11" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d07aa9a93b00c76f71bc35d598bed923f6d4f3a9ca5c24b7737ae1a292841c0" +checksum = "e1de8bc0aa9e9385ceb3bf0c152e3a9b9544f6c4a912c8ae504e80c1f0368603" [[package]] name = "cc" -version = "1.2.34" +version = "1.2.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" +checksum = "80f41ae168f955c12fb8960b057d70d0ca153fb83182b57d86380443527be7e9" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", @@ -594,9 +595,9 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.20.2" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d458d63f0f0f482c8da9b7c8b76c21bd885a02056cc94c6404d861ca2b8206" +checksum = "1a2c5f3bf25ec225351aa1c8e230d04d880d3bd89dea133537dafad4ae291e5c" dependencies = [ "smallvec", "target-lexicon", @@ -684,14 +685,14 @@ dependencies = [ [[package]] name = "config" -version = "0.15.14" +version = "0.15.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa4092bf3922a966e2bd74640b80f36c73eaa7251a4fd0fbcda1f8a4de401352" +checksum = "cef036f0ecf99baef11555578630e2cca559909b4c50822dbba828c252d21c49" dependencies = [ "convert_case 0.6.0", "pathdiff", - "serde", - "toml 0.9.2", + "serde_core", + "toml 0.9.7", "winnow", ] @@ -737,12 +738,6 @@ dependencies = [ name = "const_str_slice_concat" version = "0.1.0" -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - [[package]] name = "convert_case" version = "0.6.0" @@ -856,9 +851,9 @@ checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" dependencies = [ "powerfmt", ] @@ -874,19 +869,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "derive_more" -version = "0.99.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" -dependencies = [ - "convert_case 0.4.0", - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.106", -] - [[package]] name = "derive_more" version = "2.0.1" @@ -918,6 +900,80 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dioxus-cli-config" +version = "0.7.0-rc.0" +source = "git+https://github.com/dioxuslabs/dioxus#eef4db67b1164934eb29b7ac4d4bcd693bba25a5" + +[[package]] +name = "dioxus-core" +version = "0.7.0-rc.0" +source = "git+https://github.com/dioxuslabs/dioxus#eef4db67b1164934eb29b7ac4d4bcd693bba25a5" +dependencies = [ + "const_format", + "dioxus-core-types", + "futures-channel", + "futures-util", + "generational-box", + "longest-increasing-subsequence", + "rustc-hash 2.1.1", + "rustversion", + "serde", + "slab", + "slotmap", + "subsecond", + "tracing", + "warnings", +] + +[[package]] +name = "dioxus-core-types" +version = "0.7.0-rc.0" +source = "git+https://github.com/dioxuslabs/dioxus#eef4db67b1164934eb29b7ac4d4bcd693bba25a5" + +[[package]] +name = "dioxus-devtools" +version = "0.7.0-rc.0" +source = "git+https://github.com/dioxuslabs/dioxus#eef4db67b1164934eb29b7ac4d4bcd693bba25a5" +dependencies = [ + "dioxus-cli-config", + "dioxus-core", + "dioxus-devtools-types", + "dioxus-signals", + "serde", + "serde_json", + "subsecond", + "thiserror 2.0.16", + "tracing", + "tungstenite 0.27.0", + "warnings", +] + +[[package]] +name = "dioxus-devtools-types" +version = "0.7.0-rc.0" +source = "git+https://github.com/dioxuslabs/dioxus#eef4db67b1164934eb29b7ac4d4bcd693bba25a5" +dependencies = [ + "dioxus-core", + "serde", + "subsecond-types", +] + +[[package]] +name = "dioxus-signals" +version = "0.7.0-rc.0" +source = "git+https://github.com/dioxuslabs/dioxus#eef4db67b1164934eb29b7ac4d4bcd693bba25a5" +dependencies = [ + "dioxus-core", + "futures-channel", + "futures-util", + "generational-box", + "parking_lot", + "rustc-hash 2.1.1", + "tracing", + "warnings", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -990,12 +1046,12 @@ checksum = "a1731451909bde27714eacba19c2566362a7f35224f52b153d3f42cf60f72472" [[package]] name = "errno" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -1025,6 +1081,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "find-msvc-tools" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" + [[package]] name = "flate2" version = "1.1.2" @@ -1174,6 +1236,15 @@ dependencies = [ "slab", ] +[[package]] +name = "generational-box" +version = "0.7.0-rc.0" +source = "git+https://github.com/dioxuslabs/dioxus#eef4db67b1164934eb29b7ac4d4bcd693bba25a5" +dependencies = [ + "parking_lot", + "tracing", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1207,7 +1278,7 @@ dependencies = [ "js-sys", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasi 0.14.7+wasi-0.2.4", "wasm-bindgen", ] @@ -1227,7 +1298,7 @@ dependencies = [ "gobject-sys", "libc", "system-deps", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1377,9 +1448,15 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hashbrown" -version = "0.15.4" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" + +[[package]] +name = "hashbrown" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" [[package]] name = "heapless" @@ -1555,9 +1632,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ "base64", "bytes", @@ -1692,19 +1769,19 @@ checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" [[package]] name = "indexmap" -version = "2.11.0" +version = "2.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", - "hashbrown 0.15.4", + "hashbrown 0.16.0", ] [[package]] name = "insta" -version = "1.43.1" +version = "1.43.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "154934ea70c58054b556dd430b99a98c2a7ff5309ac9891597e339b5c28f4371" +checksum = "46fdb647ebde000f43b5b53f773c30cf9b0cb4300453208713fa38b2c70935a0" dependencies = [ "console", "once_cell", @@ -1780,9 +1857,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "852f13bec5eba4ba9afbeb93fd7c13fe56147f055939ae21c43a29a0ecb2702e" dependencies = [ "once_cell", "wasm-bindgen", @@ -1801,6 +1878,8 @@ dependencies = [ "any_spawner", "base64", "cfg-if", + "dioxus-cli-config", + "dioxus-devtools", "either_of", "futures", "getrandom 0.3.3", @@ -1824,6 +1903,7 @@ dependencies = [ "serde_qs", "server_fn", "slotmap", + "subsecond", "tachys", "thiserror 2.0.16", "throw_error", @@ -2066,6 +2146,16 @@ version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +[[package]] +name = "libloading" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +dependencies = [ + "cfg-if", + "windows-targets 0.53.3", +] + [[package]] name = "linear-map" version = "1.2.0" @@ -2074,9 +2164,9 @@ checksum = "bfae20f6b19ad527b550c223fddc3077a547fc70cda94b9b566575423fd303ee" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" @@ -2113,9 +2203,15 @@ dependencies = [ [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "longest-increasing-subsequence" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "b3bd0dd2cd90571056fdb71f6275fada10131182f84899f4b2a916e565d81d86" [[package]] name = "lru-slab" @@ -2158,6 +2254,24 @@ version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +[[package]] +name = "memfd" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" +dependencies = [ + "rustix", +] + +[[package]] +name = "memmap2" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" +dependencies = [ + "libc", +] + [[package]] name = "mime" version = "0.3.17" @@ -2482,9 +2596,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" dependencies = [ "zerovec", ] @@ -2516,11 +2630,11 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit", + "toml_edit 0.23.6", ] [[package]] @@ -2624,9 +2738,9 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ "bytes", "cfg_aliases", @@ -2635,7 +2749,7 @@ dependencies = [ "quinn-udp", "rustc-hash 2.1.1", "rustls", - "socket2 0.5.10", + "socket2 0.6.0", "thiserror 2.0.16", "tokio", "tracing", @@ -2644,9 +2758,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.12" +version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ "bytes", "getrandom 0.3.3", @@ -2665,16 +2779,16 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.13" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.0", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -2769,6 +2883,7 @@ dependencies = [ "send_wrapper", "serde", "slotmap", + "subsecond", "thiserror 2.0.16", "tokio", "tokio-test", @@ -2926,7 +3041,7 @@ checksum = "19f5c3e5da784cd8c69d32cdc84673f3204536ca56e1fa01be31a74b92c932ac" dependencies = [ "bytecheck", "bytes", - "hashbrown 0.15.4", + "hashbrown 0.15.5", "indexmap", "munge", "ptr_meta", @@ -3014,22 +3129,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.8" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] name = "rustls" -version = "0.23.31" +version = "0.23.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" dependencies = [ "once_cell", "ring", @@ -3051,9 +3166,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.4" +version = "0.103.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +checksum = "8572f3c2cb9934231157b45499fc41e1f58c589fdfb81a844ba873265e80f8eb" dependencies = [ "ring", "rustls-pki-types", @@ -3092,11 +3207,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -3126,9 +3241,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.14.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", @@ -3136,9 +3251,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "send_wrapper" @@ -3151,10 +3266,11 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.219" +version = "1.0.225" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "fd6c24dee235d0da097043389623fb913daddf92c76e9f5a1db88607a0bcbd1d" dependencies = [ + "serde_core", "serde_derive", ] @@ -3190,11 +3306,20 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "serde_core" +version = "1.0.225" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "659356f9a0cb1e529b24c01e43ad2bdf520ec4ceaf83047b83ddcc2251f96383" +dependencies = [ + "serde_derive", +] + [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.225" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "0ea936adf78b1f766949a4977b91d2f5595825bd6ec079aa9543ad2685fc4516" dependencies = [ "proc-macro2", "quote", @@ -3203,24 +3328,26 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.143" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] name = "serde_path_to_error" -version = "0.1.17" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" dependencies = [ "itoa", "serde", + "serde_core", ] [[package]] @@ -3245,11 +3372,11 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" +checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -3463,6 +3590,7 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" dependencies = [ + "serde", "version_check", ] @@ -3507,6 +3635,32 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "subsecond" +version = "0.7.0-rc.0" +source = "git+https://github.com/dioxuslabs/dioxus#eef4db67b1164934eb29b7ac4d4bcd693bba25a5" +dependencies = [ + "js-sys", + "libc", + "libloading", + "memfd", + "memmap2", + "serde", + "subsecond-types", + "thiserror 2.0.16", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "subsecond-types" +version = "0.7.0-rc.0" +source = "git+https://github.com/dioxuslabs/dioxus#eef4db67b1164934eb29b7ac4d4bcd693bba25a5" +dependencies = [ + "serde", +] + [[package]] name = "subtle" version = "2.6.1" @@ -3642,15 +3796,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.21.0" +version = "3.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" +checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53" dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -3711,9 +3865,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.41" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", "itoa", @@ -3726,15 +3880,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" dependencies = [ "num-conv", "time-core", @@ -3808,9 +3962,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.2" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +checksum = "05f63835928ca123f1bef57abbcd23bb2ba0ac9ae1235f1e65bda0d06e7786bd" dependencies = [ "rustls", "tokio", @@ -3887,19 +4041,19 @@ dependencies = [ "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.11", - "toml_edit", + "toml_edit 0.22.27", ] [[package]] name = "toml" -version = "0.9.2" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed0aee96c12fa71097902e0bb061a5e1ebd766a6636bb605ba401c45c1650eac" +checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0" dependencies = [ "indexmap", - "serde", - "serde_spanned 1.0.0", - "toml_datetime 0.7.0", + "serde_core", + "serde_spanned 1.0.2", + "toml_datetime 0.7.2", "toml_parser", "toml_writer", "winnow", @@ -3916,11 +4070,11 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.0" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" +checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -3936,20 +4090,32 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_edit" +version = "0.23.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" +dependencies = [ + "indexmap", + "toml_datetime 0.7.2", + "toml_parser", + "winnow", +] + [[package]] name = "toml_parser" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10" +checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" dependencies = [ "winnow", ] [[package]] name = "toml_writer" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" +checksum = "d163a63c116ce562a22cda521fcc4d79152e7aba014456fb5eb442f6d6a10109" [[package]] name = "tower" @@ -4047,9 +4213,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "trybuild" -version = "1.0.110" +version = "1.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e257d7246e7a9fd015fb0b28b330a8d4142151a33f03e6a497754f4b1f6a8e" +checksum = "0ded9fdb81f30a5708920310bfcd9ea7482ff9cba5f54601f7a19a877d5c2392" dependencies = [ "glob", "serde", @@ -4057,7 +4223,7 @@ dependencies = [ "serde_json", "target-triple", "termcolor", - "toml 0.9.2", + "toml 0.9.7", ] [[package]] @@ -4128,9 +4294,9 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-segmentation" @@ -4152,13 +4318,14 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -4181,9 +4348,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.18.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ "getrandom 0.3.3", "js-sys", @@ -4233,6 +4400,28 @@ dependencies = [ "try-lock", ] +[[package]] +name = "warnings" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64f68998838dab65727c9b30465595c6f7c953313559371ca8bf31759b3680ad" +dependencies = [ + "pin-project", + "tracing", + "warnings-macro", +] + +[[package]] +name = "warnings-macro" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59195a1db0e95b920366d949ba5e0d3fc0e70b67c09be15ce5abb790106b0571" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -4241,30 +4430,40 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.2+wasi-0.2.4" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "ab10a69fbd0a177f5f649ad4d8d3305499c42bab9aef2f7ff592d0ec8f833819" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "0bb702423545a6007bbc368fde243ba47ca275e549c8a28617f56f6ba53b1d1c" dependencies = [ "bumpalo", "log", @@ -4276,9 +4475,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "a0b221ff421256839509adbb55998214a70d829d3a28c69b4a6672e9d2a42f67" dependencies = [ "cfg-if", "js-sys", @@ -4289,9 +4488,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "fc65f4f411d91494355917b605e1480033152658d71f722a90647f56a70c88a0" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4299,9 +4498,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32" dependencies = [ "proc-macro2", "quote", @@ -4312,18 +4511,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "293c37f4efa430ca14db3721dfbe48d8c33308096bd44d80ebaa775ab71ba1cf" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-bindgen-test" -version = "0.3.50" +version = "0.3.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66c8d5e33ca3b6d9fa3b4676d774c5778031d27a578c2b007f905acf816152c3" +checksum = "aee0a0f5343de9221a0d233b04520ed8dc2e6728dce180b1dcd9288ec9d9fa3c" dependencies = [ "js-sys", "minicov", @@ -4334,9 +4533,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.50" +version = "0.3.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17d5042cc5fa009658f9a7333ef24291b1291a25b6382dd68862a7f3b969f69b" +checksum = "a369369e4360c2884c3168d22bded735c43cccae97bbc147586d4b480edd138d" dependencies = [ "proc-macro2", "quote", @@ -4379,9 +4578,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "fbe734895e869dc429d78c4b433f8d17d95f8d05317440b4fad5ab2d33e596dc" dependencies = [ "js-sys", "wasm-bindgen", @@ -4408,13 +4607,25 @@ dependencies = [ [[package]] name = "winapi-util" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.0", ] +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + [[package]] name = "windows-sys" version = "0.52.0" @@ -4439,7 +4650,16 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.2", + "windows-targets 0.53.3", +] + +[[package]] +name = "windows-sys" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" +dependencies = [ + "windows-link 0.2.0", ] [[package]] @@ -4460,10 +4680,11 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.2" +version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ + "windows-link 0.1.3", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -4580,13 +4801,10 @@ dependencies = [ ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags", -] +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" @@ -4632,18 +4850,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", @@ -4730,9 +4948,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.15+zstd.1.5.7" +version = "2.0.16+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index 07a702fed0..7553b1a498 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -170,6 +170,9 @@ async-lock = { default-features = false, version = "3.4.1" } base16 = { default-features = false, version = "0.2.1" } digest = { default-features = false, version = "0.10.7" } sha2 = { default-features = false, version = "0.10.8" } +subsecond = { default-features = false, git = "https://github.com/dioxuslabs/dioxus" } +dioxus-cli-config = { default-features = false, git = "https://github.com/dioxuslabs/dioxus" } +dioxus-devtools = { default-features = false, git = "https://github.com/dioxuslabs/dioxus" } [profile.release] codegen-units = 1 diff --git a/examples/subsecond_hot_patch/.gitignore b/examples/subsecond_hot_patch/.gitignore new file mode 100644 index 0000000000..80aab8ea95 --- /dev/null +++ b/examples/subsecond_hot_patch/.gitignore @@ -0,0 +1,7 @@ +# Generated by Cargo +# will have compiled files and executables +/target +.DS_Store + +# These are backup files generated by rustfmt +**/*.rs.bk diff --git a/examples/subsecond_hot_patch/Cargo.toml b/examples/subsecond_hot_patch/Cargo.toml new file mode 100644 index 0000000000..bba28a1c23 --- /dev/null +++ b/examples/subsecond_hot_patch/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "subsecond_hot_patch" +version = "0.1.0" +authors = ["Greg Johnston <greg.johnston@gmail.com>"] +edition = "2021" + +[dependencies] +leptos = { path = "../../leptos", features = ["csr", "subsecond"] } +leptos_router = { path = "../../router" } + +[features] +default = ["web"] +web = [] diff --git a/examples/subsecond_hot_patch/Dioxus.toml b/examples/subsecond_hot_patch/Dioxus.toml new file mode 100644 index 0000000000..f6e9a355f3 --- /dev/null +++ b/examples/subsecond_hot_patch/Dioxus.toml @@ -0,0 +1,21 @@ +[application] + +[web.app] + +# HTML title tag content +title = "ltest" + +# include `assets` in web platform +[web.resource] + +# Additional CSS style files +style = [] + +# Additional JavaScript files +script = [] + +[web.resource.dev] + +# Javascript code file +# serve: [dev-server] only +script = [] diff --git a/examples/subsecond_hot_patch/Makefile.toml b/examples/subsecond_hot_patch/Makefile.toml new file mode 100644 index 0000000000..6fe53288aa --- /dev/null +++ b/examples/subsecond_hot_patch/Makefile.toml @@ -0,0 +1 @@ +extend = [{ path = "../cargo-make/main.toml" }] diff --git a/examples/subsecond_hot_patch/README.md b/examples/subsecond_hot_patch/README.md new file mode 100644 index 0000000000..992a4dbdb9 --- /dev/null +++ b/examples/subsecond_hot_patch/README.md @@ -0,0 +1,31 @@ +# Hot Patching with `dx` + +This is an experimental example exploring how to combine Leptos with the binary hot-patching provided by Dioxus's `subsecond` library and `dx` cli. + +### Serving Your App + +This requires installing the Dioxus CLI version 0.7.0. At the time I'm writing this README, that does not yet have a stable release. Once `dioxus-cli` 0.7.0 has been released, you should use the latest stable release. Until then, I'd suggest installing from git: + +```sh +cargo install dioxus-cli --git https://github.com/DioxusLabs/dioxus +``` + +Then you can run the example with `dx serve --hot-patch --platform web`. + +### Hot Patching + +Changes to the your application should be reflected in your app without a full rebuild and reload. + +### Limitatations + +Currently we only support hot-patching for reactive view functions. You probably want to use `AnyView` (via `.into_any()`) on any views that will be hot-patched, so they can be rebuilt correctly despite their types changing when the structure of the view tree changes. + +If you are using `leptos_router` this actually works quite well, as every route’s view is erased to `AnyView` and the router itself is a reactive view function: in other words, changes inside any route should be hot-patched in any case. + +Note that any hot-patch will cause all render effects to run again. This means that some client-side state (like the values of signals) will be wiped out. + +### Build Tooling + +The preference of the Dioxus team is that all hot-patching work that uses their `subsecond` also use `dioxus-cli`. As this demo shows, it's completely possible to use `dioxus-cli` to build and run a Leptos project. We do not plan to build `subsecond` into our own build tooling at this time. + +**This is an experiment/POC. It is being published because members of the community have found it useful and have asked for the support to be merged in its current state. Further development and bugfixes are a relatively low priority at this time.** diff --git a/examples/subsecond_hot_patch/assets/favicon.ico b/examples/subsecond_hot_patch/assets/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..eed0c09735ab94e724c486a053c367cf7ee3d694 GIT binary patch literal 132770 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 literal 0 HcmV?d00001 diff --git a/examples/subsecond_hot_patch/assets/header.svg b/examples/subsecond_hot_patch/assets/header.svg new file mode 100644 index 0000000000..59c96f2f2e --- /dev/null +++ b/examples/subsecond_hot_patch/assets/header.svg @@ -0,0 +1,20 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1007 197"><style> +@keyframes a0_t { 0% { transform: translate(225px,339.9px) scale(0,1) translate(-89.5px,-24px); } 35% { transform: translate(225px,339.9px) scale(0,1) translate(-89.5px,-24px); animation-timing-function: cubic-bezier(.6,0,.4,1); } 45% { transform: translate(225px,339.9px) scale(1,1) translate(-89.5px,-24px); } 50% { transform: translate(225px,339.9px) scale(1,1) translate(-89.5px,-24px); animation-timing-function: cubic-bezier(.6,0,.4,1); } 60% { transform: translate(225px,339.9px) scale(0,1) translate(-89.5px,-24px); } 100% { transform: translate(225px,339.9px) scale(0,1) translate(-89.5px,-24px); } } +@keyframes a1_t { 0% { transform: translate(225px,348.9px) scale(0,1) translate(-41.5px,-11px); } 20% { transform: translate(225px,348.9px) scale(0,1) translate(-41.5px,-11px); animation-timing-function: cubic-bezier(.6,0,.4,1); } 30% { transform: translate(225px,348.9px) scale(1,1) translate(-41.5px,-11px); } 35% { transform: translate(225px,348.9px) scale(1,1) translate(-41.5px,-11px); } 45% { transform: translate(225px,348.9px) scale(0,1) translate(-41.5px,-11px); } 100% { transform: translate(225px,348.9px) scale(0,1) translate(-41.5px,-11px); } } +@keyframes a3_t { 0% { transform: translate(225px,198.9px) rotate(-90deg); } 10% { transform: translate(225px,198.9px) rotate(-90deg); } 20% { transform: translate(225px,198.9px) rotate(-90deg); animation-timing-function: cubic-bezier(.6,0,.4,1); } 30% { transform: translate(225px,198.9px) rotate(0deg); } 35% { transform: translate(225px,198.9px) rotate(0deg); } 45% { transform: translate(225px,198.9px) rotate(0deg); } 50% { transform: translate(225px,198.9px) rotate(0deg); animation-timing-function: cubic-bezier(.6,0,.4,1); } 60% { transform: translate(225px,198.9px) rotate(-90deg); } 65% { transform: translate(225px,198.9px) rotate(-90deg); } 75% { transform: translate(225px,198.9px) rotate(-90deg); } 100% { transform: translate(225px,198.9px) rotate(-90deg); } } +@keyframes a2_t { 0% { transform: translate(-200px,-167px); animation-timing-function: cubic-bezier(.6,0,.4,1); } 10% { transform: translate(-150px,-80px); } 20% { transform: translate(-150px,-80px); animation-timing-function: cubic-bezier(.6,0,.4,1); } 30% { transform: translate(-200px,-123px); } 35% { transform: translate(-200px,-123px); animation-timing-function: cubic-bezier(.6,0,.4,1); } 45% { transform: translate(-150px,-100px); } 50% { transform: translate(-150px,-100px); animation-timing-function: cubic-bezier(.6,0,.4,1); } 60% { transform: translate(-150px,-80px); } 65% { transform: translate(-150px,-80px); animation-timing-function: cubic-bezier(.6,0,.4,1); } 75% { transform: translate(-217px,-200px); } 100% { transform: translate(-217px,-200px); } } +@keyframes a2_o { 0% { opacity: 0; animation-timing-function: cubic-bezier(.6,0,.4,1); } 10% { opacity: 1; } 65% { opacity: 1; animation-timing-function: cubic-bezier(.6,0,.4,1); } 75% { opacity: 0; } 100% { opacity: 0; } } +@keyframes a2_sw { 0% { stroke-width: 30px; animation-timing-function: cubic-bezier(.6,0,.4,1); } 10% { stroke-width: 8px; } 65% { stroke-width: 8px; animation-timing-function: cubic-bezier(.6,0,.4,1); } 75% { stroke-width: 30px; } 100% { stroke-width: 30px; } } +@keyframes a2_w { 0% { width: 400px; animation-timing-function: cubic-bezier(.6,0,.4,1); } 10% { width: 300px; } 20% { width: 300px; animation-timing-function: cubic-bezier(.6,0,.4,1); } 30% { width: 400px; } 35% { width: 400px; animation-timing-function: cubic-bezier(.6,0,.4,1); } 45% { width: 300px; } 50% { width: 300px; } 60% { width: 300px; } 65% { width: 300px; animation-timing-function: cubic-bezier(.6,0,.4,1); } 75% { width: 434px; } 100% { width: 434px; } } +@keyframes a2_h { 0% { height: 334px; animation-timing-function: cubic-bezier(.6,0,.4,1); } 10% { height: 160px; } 20% { height: 160px; animation-timing-function: cubic-bezier(.6,0,.4,1); } 30% { height: 246px; } 35% { height: 246px; animation-timing-function: cubic-bezier(.6,0,.4,1); } 45% { height: 200px; } 50% { height: 200px; animation-timing-function: cubic-bezier(.6,0,.4,1); } 60% { height: 160px; } 65% { height: 160px; animation-timing-function: cubic-bezier(.6,0,.4,1); } 75% { height: 400px; } 100% { height: 400px; } } +@keyframes a4_t { 0% { transform: translate(143.5px,48.9px); } 10% { transform: translate(180.5px,98.9px); } 100% { transform: translate(180.5px,98.9px); } } +@keyframes a4_w { 0% { width: 168.5px; } 10% { width: 94.5px; } 100% { width: 94.5px; } } +@keyframes a4_h { 0% { height: 302px; } 10% { height: 202px; } 100% { height: 202px; } } +@keyframes a5_t { 0% { transform: translate(217px,324.9px); animation-timing-function: cubic-bezier(.6,0,.4,1); } 10% { transform: translate(190px,324.9px); } 20% { transform: translate(190px,324.9px); } 30% { transform: translate(190px,324.9px); } 100% { transform: translate(190px,324.9px); } } +@keyframes a5_o { 0% { opacity: 0; animation-timing-function: cubic-bezier(.6,0,.4,1); } 10% { opacity: 1; } 20% { opacity: 1; } 30% { opacity: 0; } 100% { opacity: 0; } } +@keyframes a5_w { 0% { width: 16px; animation-timing-function: cubic-bezier(.6,0,.4,1); } 10% { width: 70px; } 20% { width: 70px; } 30% { width: 70px; } 100% { width: 70px; } } +@keyframes a6_t { 0% { transform: translate(217px,324.9px); } 50% { transform: translate(217px,324.9px); animation-timing-function: cubic-bezier(.6,0,.4,1); } 60% { transform: translate(190px,324.9px); } 65% { transform: translate(190px,324.9px); } 75% { transform: translate(190px,324.9px); } 100% { transform: translate(190px,324.9px); } } +@keyframes a6_o { 0% { opacity: 0; } 50% { opacity: 0; animation-timing-function: cubic-bezier(.6,0,.4,1); } 60% { opacity: 1; } 65% { opacity: 1; animation-timing-function: cubic-bezier(.6,0,.4,1); } 75% { opacity: 0; } 100% { opacity: 0; } } +@keyframes a6_w { 0% { width: 16px; } 50% { width: 16px; animation-timing-function: cubic-bezier(.6,0,.4,1); } 60% { width: 70px; } 65% { width: 70px; } 75% { width: 70px; } 100% { width: 70px; } } +@keyframes a7_t { 0% { transform: translate(225px,198.9px) scale(1.5,1.5) translate(-42.3px,-93px); animation-timing-function: cubic-bezier(.6,0,.4,1); } 10% { transform: translate(225px,198.9px) scale(1,1) translate(-42.3px,-93px); } 20% { transform: translate(225px,198.9px) scale(1,1) translate(-42.3px,-93px); animation-timing-function: cubic-bezier(.6,0,.4,1); } 25% { transform: translate(225px,198.9px) scale(.8,.8) translate(-42.3px,-93px); animation-timing-function: cubic-bezier(.6,0,.4,1); } 30% { transform: translate(225px,198.9px) scale(1,1) translate(-42.3px,-93px); } 35% { transform: translate(225px,198.9px) scale(1,1) translate(-42.3px,-93px); animation-timing-function: cubic-bezier(.4,0,.6,1); } 45% { transform: translate(225px,198.9px) scale(.8,.8) translate(-42.3px,-93px); } 50% { transform: translate(225px,198.9px) scale(.8,.8) translate(-42.3px,-93px); animation-timing-function: cubic-bezier(.4,0,.6,1); } 55% { transform: translate(225px,198.9px) scale(.7,.7) translate(-42.3px,-93px); animation-timing-function: cubic-bezier(.4,0,.6,1); } 60% { transform: translate(225px,198.9px) scale(1,1) translate(-42.3px,-93px); } 65% { transform: translate(225px,198.9px) scale(1,1) translate(-42.3px,-93px); animation-timing-function: cubic-bezier(.4,0,.6,1); } 75% { transform: translate(225px,198.9px) scale(1.5,1.5) translate(-42.3px,-93px); } 100% { transform: translate(225px,198.9px) scale(1.5,1.5) translate(-42.3px,-93px); } } +</style><defs><symbol id="Symbol-2" preserveAspectRatio="none" width="111.5" height="186" viewBox="0 0 111.5 186" overflow="visible"><g transform="translate(8.5,4.5)"><rect x="4" width="85" height="8" fill="#d9d9d9"/><rect x="4" y="26" width="85" height="8" fill="#d9d9d9"/><rect x="25" y="56" width="43" height="8" fill="#d9d9d9"/><rect width="85" height="8" fill="#d9d9d9" transform="translate(46.5,175) rotate(180) scale(-1,1) translate(-42.5,-4)"/><rect width="85" height="8" fill="#d9d9d9" transform="translate(46.5,149) rotate(180) scale(-1,1) translate(-42.5,-4)"/><rect width="43" height="8" fill="#d9d9d9" transform="translate(46.5,119) rotate(180) scale(-1,1) translate(-21.5,-4)"/><path d="M0 4c0 53 94.5 115 94.5 169" stroke="#3cc4dc" stroke-width="17" stroke-linecap="square" fill="none"/><path d="M94.5 4c0 53-94.5 115-94.5 169" stroke="#fb422d" stroke-width="17" stroke-linecap="square" fill="none"/></g></symbol><symbol id="Symbol-5" preserveAspectRatio="none" width="84.5" height="186" viewBox="0 0 84.5 186" overflow="visible"><g transform="translate(42.3,93) scale(5.352,6.9) translate(-16,60)"><g transform="translate(-34.9,-125.8)"><g transform="translate(24,5)"><path d="M33 46.5c0 3.1-0.8 5.5-2.2 7.4c-1.4 1.9-3.3 3.6-5.4 5.4c-2.2 1.7-4.5 3.6-6.3 6.2c-1.8 2.5-3.1 5.5-3.1 9.5h4.7c0-3.1 .9-5.4 2.2-7.3c1.4-1.9 3.3-3.5 5.5-5.3c2.1-1.8 4.4-3.7 6.2-6.2c1.8-2.6 3.1-5.6 3.1-9.7Z" fill="#e96020"/><path d="M20.4 70.7c-0.6 0-1 .5-1 1.1c0 .7 .4 1.2 1 1.2h12.9c.7 0 1.2-0.5 1.2-1.2c0-0.6-0.5-1.1-1.2-1.1Z" fill="#2d323b"/><path d="M21.8 66.6c-0.7 0-1 .6-1 1.2c0 .6 .3 1 .9 1h10.3c.6 0 1.1-0.5 1.1-1.1c0-0.6-0.5-1.1-1.1-1.1Z" fill="#2d323b"/><path d="M21.8 53c-0.7 0-1.2 .5-1.1 1.1c0 .6 .4 1.1 1.1 1.1h10.2c.6 0 .9-0.6 .9-1.2c0-0.6-0.3-1-0.9-1Z" fill="#2d323b"/><path d="M20.4 48.8c-0.6 0-1.2 .5-1.2 1.1c0 .7 .6 1.2 1.2 1.2h12.9c.7 0 1-0.5 1-1.1c0-0.7-0.3-1.2-1-1.2Z" fill="#2d323b"/><path d="M16 46.5c0 4.1 1.3 7.1 3.1 9.7c1.8 2.5 4.1 4.4 6.3 6.2c2.1 1.8 4 3.6 5.4 5.5c1.4 1.9 2.2 4 2.2 7.1h4.7c0-4-1.3-7.1-3.1-9.6c-1.8-2.5-4.1-4.4-6.2-6.2c-2.2-1.8-4.1-3.4-5.5-5.3c-1.3-1.9-2.2-4.3-2.2-7.4Z" fill="#00a8d6"/></g></g></g></symbol></defs><rect width="1007" height="197" stroke="#0f1116" fill="#0f1116" stroke-width="0"/><path fill="#fff" d="M83.3 106.1v-65.7h17c4.2 0 7.9 .8 11 2.4c3.1 1.6 5.5 3.9 7.2 6.9c1.7 2.9 2.6 6.4 2.6 10.4v26.2c0 4-0.9 7.5-2.6 10.5c-1.7 3-4.1 5.3-7.2 6.9c-3.1 1.6-6.8 2.4-11 2.4Zm8.1-7.2h8.9c4 0 7.1-1.1 9.3-3.3c2.3-2.3 3.4-5.3 3.4-9.3v-26.2c0-3.9-1.1-7-3.4-9.2c-2.2-2.2-5.3-3.3-9.3-3.3h-8.9Zm45.3 7.2v-7.4h17.5v-34.7h-15.7v-7.4h23.8v42.1h16.7v7.4Zm20.7-58.4c-2 0-3.6-0.5-4.7-1.5c-1.2-1.1-1.7-2.4-1.7-4.2c0-1.8 .5-3.2 1.7-4.3c1.1-1 2.7-1.5 4.7-1.5c1.9 0 3.5 .5 4.6 1.5c1.2 1.1 1.7 2.5 1.7 4.3c0 1.8-0.5 3.1-1.7 4.2c-1.1 1-2.7 1.5-4.6 1.5Zm52.6 59.1c-6 0-10.7-1.7-14.2-5.1c-3.4-3.4-5.1-8-5.1-13.8v-13.1c0-5.9 1.7-10.5 5.1-13.9c3.5-3.3 8.2-5 14.2-5c6 0 10.7 1.7 14.2 5c3.4 3.4 5.2 8 5.2 13.8v13.2c0 5.8-1.8 10.4-5.2 13.8c-3.5 3.4-8.2 5.1-14.2 5.1Zm0-7.2c3.5 0 6.3-1 8.3-3c2-1.9 3-4.8 3-8.7v-13.1c0-3.9-1-6.8-3-8.8c-2-1.9-4.8-2.9-8.3-2.9c-3.5 0-6.2 1-8.2 2.9c-2 2-3 4.9-3 8.8v13.1c0 3.9 1 6.8 3 8.7c2 2 4.7 3 8.2 3Zm31.5 6.5l17.6-25.5l-16.5-24h9.5l9.8 15.3c.5 .7 .9 1.4 1.2 2.2c.4 .7 .8 1.4 1 1.8c.2-0.4 .5-1.1 .8-1.8c.4-0.8 .8-1.5 1.3-2.2l9.9-15.3h9.4l-16.5 24.1l17.5 25.4h-9.5l-10.7-16.2c-0.4-0.7-0.8-1.4-1.2-2.3c-0.4-0.9-0.7-1.6-1-2.1c-0.2 .5-0.6 1.2-1 2.1c-0.5 .9-1 1.6-1.4 2.3l-10.7 16.2Zm76.4 .9c-5.7 0-10.2-1.7-13.7-5.1c-3.4-3.4-5.1-8-5.1-13.8v-31.5h8.1v31.5c0 3.7 1 6.6 2.9 8.7c1.9 2.1 4.5 3.1 7.8 3.1c3.4 0 6-1 8-3.1c1.9-2.1 2.9-5 2.9-8.7v-31.5h8.1v31.5c0 5.8-1.7 10.4-5.2 13.8c-3.5 3.4-8.1 5.1-13.8 5.1Zm52.6-0.2c-5.1 0-9.3-1.2-12.5-3.5c-3.3-2.3-4.9-5.5-4.9-9.6h8.3c0 1.8 .9 3.3 2.6 4.3c1.8 1.1 4 1.6 6.6 1.6h3.8c3.2 0 5.6-0.6 7.1-1.9c1.6-1.3 2.4-3 2.4-5.2c0-2.1-0.8-3.8-2.2-5c-1.5-1.2-3.6-2-6.4-2.5l-6.6-1c-5-0.9-8.6-2.4-10.9-4.7c-2.4-2.3-3.5-5.5-3.5-9.5c0-4.5 1.4-7.9 4.3-10.3c2.9-2.4 7.1-3.6 12.6-3.6h3.4c5 0 9.1 1.1 12.1 3.4c3 2.2 4.6 5.2 4.6 8.9h-8.3c0-1.6-0.8-2.8-2.3-3.7c-1.5-1-3.6-1.4-6.3-1.4h-3.4c-2.8 0-5 .6-6.5 1.7c-1.5 1.2-2.3 2.9-2.3 5c0 3.6 2.5 5.8 7.6 6.7l6.7 1.1c5.3 .8 9.2 2.4 11.7 4.6c2.4 2.3 3.6 5.6 3.6 9.9c0 4.6-1.5 8.2-4.4 10.8c-2.9 2.6-7.3 3.9-13.2 3.9Z"/><path fill="#fff" d="M91 164c-2 0-3.6-0.6-4.7-1.7c-1.1-1.1-1.6-2.7-1.6-4.6v-15.9h2.7v15.9c0 1.2 .3 2.2 .9 2.8c.6 .7 1.5 1.1 2.7 1.1c1.2 0 2.1-0.4 2.7-1.1c.6-0.7 .9-1.6 .9-2.8v-15.9h2.7v15.9c0 2-0.5 3.5-1.6 4.6c-1.1 1.1-2.6 1.7-4.7 1.7Zm17.5-0.1c-1.7 0-3.1-0.3-4.2-1.1c-1.1-0.8-1.6-1.9-1.6-3.2h2.8c0 .6 .3 1.1 .8 1.4c.6 .4 1.4 .5 2.2 .5h1.3c1.1 0 1.9-0.2 2.4-0.6c.5-0.4 .8-1 .8-1.7c0-0.7-0.3-1.3-0.8-1.7c-0.5-0.4-1.2-0.7-2.1-0.8l-2.2-0.4c-1.7-0.3-2.9-0.8-3.6-1.5c-0.8-0.8-1.2-1.9-1.2-3.2c0-1.5 .5-2.6 1.4-3.4c1-0.8 2.4-1.2 4.2-1.2h1.2c1.6 0 3 .3 4 1.1c1 .7 1.5 1.7 1.5 3h-2.7c0-0.6-0.3-1-0.8-1.3c-0.5-0.3-1.2-0.4-2.1-0.4h-1.1c-1 0-1.7 .2-2.2 .5c-0.5 .4-0.8 1-0.8 1.7c0 1.2 .9 1.9 2.6 2.2l2.2 .4c1.8 .3 3.1 .8 3.9 1.5c.8 .8 1.2 1.9 1.2 3.3c0 1.6-0.5 2.8-1.5 3.6c-0.9 .9-2.4 1.3-4.4 1.3Zm18.5 .1c-1.3 0-2.4-0.3-3.4-0.8c-1-0.5-1.7-1.2-2.3-2.2c-0.5-0.9-0.8-2-0.8-3.3v-4.5c0-1.3 .3-2.4 .8-3.3c.6-1 1.3-1.7 2.3-2.2c1-0.5 2.1-0.8 3.4-0.8c1.3 0 2.4 .3 3.4 .8c1 .5 1.7 1.2 2.3 2.2c.5 .9 .8 2 .8 3.3l-0.1 2.9h-10.2v1.6c0 1.3 .3 2.3 1 3c.6 .7 1.6 1 2.8 1c1 0 1.9-0.2 2.5-0.5c.7-0.4 1-1 1.2-1.7h2.7c-0.2 1.4-0.9 2.5-2 3.3c-1.2 .8-2.7 1.2-4.4 1.2Zm-3.8-9.9h7.6v-0.9c0-1.3-0.3-2.3-1-3c-0.6-0.7-1.6-1-2.8-1c-1.2 0-2.2 .3-2.8 1c-0.7 .7-1 1.7-1 3Zm16.1 9.6v-16.5h2.7v3.2c.1-1.1 .6-2 1.4-2.6c.7-0.6 1.7-0.9 3-0.9c1.7 0 3.1 .5 4.1 1.6c1 1.1 1.5 2.6 1.5 4.5v1.1h-2.7v-0.9c0-1.3-0.3-2.2-0.9-2.9c-0.7-0.7-1.6-1.1-2.8-1.1c-2.4 0-3.6 1.4-3.6 4v10.5Zm35.8 0v-2.5h4.5v-16.9h-4.5v-2.5h11.8l-0.1 2.5h-4.4v16.9h4.4v2.5Zm17.7 0v-16.5h2.7v3.2c.1-1.1 .6-2 1.3-2.6c.8-0.6 1.8-0.9 3.1-0.9c1.6 0 3 .5 3.9 1.5c1 1.1 1.5 2.4 1.5 4.2v11.1h-2.7v-10.8c0-1.2-0.3-2.1-0.9-2.7c-0.7-0.6-1.5-1-2.6-1c-1.1 0-2 .4-2.7 1.1c-0.6 .6-0.9 1.6-0.9 2.9v10.5Zm26 0c-1.4 0-2.4-0.4-3.3-1.2c-0.8-0.8-1.2-1.8-1.2-3.1v-9.7h-4.7v-2.5h4.7v-4.7h2.7v4.7h6.6v2.5h-6.6v9.7c0 .5 .2 1 .5 1.3c.3 .4 .8 .5 1.3 .5h4.5v2.5Zm16.2 .3c-1.3 0-2.4-0.3-3.4-0.8c-1-0.5-1.7-1.2-2.3-2.2c-0.5-0.9-0.7-2-0.7-3.3v-4.5c0-1.3 .2-2.4 .7-3.3c.6-1 1.3-1.7 2.3-2.2c1-0.5 2.1-0.8 3.4-0.8c1.3 0 2.4 .3 3.4 .8c1 .5 1.7 1.2 2.3 2.2c.5 .9 .8 2 .8 3.3l-0.1 2.9h-10.2v1.6c0 1.3 .3 2.3 1 3c.6 .7 1.6 1 2.8 1c1 0 1.9-0.2 2.5-0.5c.7-0.4 1-1 1.2-1.7h2.7c-0.2 1.4-0.9 2.5-2 3.3c-1.2 .8-2.7 1.2-4.4 1.2Zm-3.8-9.9h7.6v-0.9c0-1.3-0.3-2.3-1-3c-0.6-0.7-1.6-1-2.8-1c-1.2 0-2.2 .3-2.8 1c-0.7 .7-1 1.7-1 3Zm16.1 9.6v-16.5h2.7v3.2c.1-1.1 .6-2 1.4-2.6c.7-0.6 1.7-0.9 3-0.9c1.7 0 3.1 .5 4.1 1.6c1 1.1 1.5 2.6 1.5 4.5v1.1h-2.7v-0.9c0-1.3-0.3-2.2-0.9-2.9c-0.7-0.7-1.6-1.1-2.8-1.1c-2.4 0-3.6 1.4-3.6 4v10.5Zm21.3 0v-12.4h-4.8v-2.4h4.8v-2.9c0-1.3 .4-2.3 1.2-3.1c.9-0.7 2-1.1 3.4-1.1h4.9v2.5h-4.9c-1.3 0-1.9 .5-1.9 1.7v2.9h6.8v2.4h-6.8v12.4Zm19 .3c-1.8 0-3.1-0.4-4.1-1.3c-1-0.9-1.5-2.1-1.5-3.6c0-1.6 .5-2.8 1.5-3.7c1-0.9 2.3-1.3 4-1.3h5.1v-1.6c0-1-0.3-1.8-0.9-2.4c-0.6-0.5-1.5-0.8-2.5-0.8c-1 0-1.8 .2-2.5 .6c-0.6 .4-1 1-1.2 1.7h-2.7c.1-1 .4-1.8 1-2.5c.6-0.7 1.3-1.2 2.2-1.6c1-0.4 2-0.6 3.2-0.6c1.9 0 3.4 .5 4.5 1.5c1.1 1 1.6 2.3 1.6 4v11.3h-2.6v-3.1h-0.1c-0.1 1-0.6 1.9-1.5 2.5c-0.9 .6-2.1 .9-3.5 .9Zm.6-2.1c1.3 0 2.4-0.3 3.2-1c.8-0.6 1.2-1.4 1.2-2.4v-2.4h-5c-0.9 0-1.6 .3-2.2 .8c-0.5 .5-0.7 1.1-0.7 2c0 .9 .3 1.6 .9 2.2c.6 .5 1.5 .8 2.6 .8Zm18.9 2.1c-2 0-3.6-0.6-4.8-1.7c-1.2-1.1-1.7-2.6-1.7-4.6v-4.5c0-2 .5-3.5 1.7-4.6c1.2-1.1 2.8-1.7 4.8-1.7c1.9 0 3.5 .5 4.6 1.5c1.2 1 1.8 2.4 1.8 4.2h-2.7c0-1.1-0.4-1.9-1-2.4c-0.7-0.6-1.6-0.9-2.7-0.9c-1.2 0-2.1 .3-2.8 1c-0.7 .7-1.1 1.6-1.1 2.9l.1 4.5c-0.1 1.2 .3 2.2 1 2.9c.7 .7 1.6 1 2.8 1c1.1 0 2-0.3 2.7-0.9c.6-0.6 1-1.4 1-2.4h2.7c0 1.8-0.6 3.2-1.8 4.2c-1.1 1-2.7 1.5-4.6 1.5Zm17.9 0c-1.3 0-2.4-0.3-3.4-0.8c-1-0.5-1.7-1.2-2.3-2.2c-0.5-0.9-0.7-2-0.7-3.3v-4.5c0-1.3 .2-2.4 .7-3.3c.6-1 1.3-1.7 2.3-2.2c1-0.5 2.1-0.8 3.4-0.8c1.3 0 2.4 .3 3.4 .8c1 .5 1.7 1.2 2.3 2.2c.5 .9 .8 2 .8 3.3v2.9h-10.3v1.6c0 1.3 .3 2.3 1 3c.6 .7 1.6 1 2.8 1c1 0 1.9-0.2 2.5-0.5c.7-0.4 1.1-1 1.2-1.7h2.7c-0.2 1.4-0.9 2.5-2 3.3c-1.2 .8-2.7 1.2-4.4 1.2Zm-3.8-9.9h7.6v-0.9c0-1.3-0.3-2.3-1-3c-0.6-0.7-1.6-1-2.8-1c-1.2 0-2.2 .3-2.8 1c-0.7 .7-1 1.7-1 3Zm21.3 9.8c-1.7 0-3.1-0.3-4.2-1.1c-1.1-0.8-1.6-1.9-1.6-3.2h2.8c0 .6 .3 1.1 .8 1.4c.6 .4 1.4 .5 2.3 .5h1.2c1.1 0 1.9-0.2 2.4-0.6c.5-0.4 .8-1 .8-1.7c0-0.7-0.3-1.3-0.8-1.7c-0.5-0.4-1.2-0.7-2.1-0.8l-2.2-0.4c-1.6-0.3-2.9-0.8-3.6-1.5c-0.8-0.8-1.2-1.9-1.2-3.2c0-1.5 .5-2.6 1.4-3.4c1-0.8 2.4-1.2 4.2-1.2h1.2c1.7 0 3 .3 4 1.1c1 .7 1.5 1.7 1.5 3h-2.7c0-0.6-0.3-1-0.8-1.3c-0.5-0.3-1.2-0.4-2.1-0.4h-1.1c-1 0-1.7 .2-2.2 .5c-0.5 .4-0.8 1-0.8 1.7c0 1.2 .9 1.9 2.6 2.2l2.2 .4c1.8 .3 3.1 .8 3.9 1.5c.8 .8 1.2 1.9 1.2 3.3c0 1.6-0.5 2.8-1.5 3.6c-0.9 .9-2.4 1.3-4.3 1.3Zm38.3-0.2c-1.4 0-2.4-0.4-3.3-1.2c-0.8-0.8-1.2-1.8-1.2-3.2v-9.6h-4.6v-2.5h4.6v-4.7h2.7v4.7h6.6v2.5h-6.6v9.6c0 .6 .2 1.1 .5 1.4c.3 .4 .8 .5 1.3 .5h4.5v2.5Zm10 0v-21.9h2.7v8.6c.1-1.2 .6-2 1.3-2.6c.8-0.6 1.8-0.9 3.1-0.9c1.6 0 3 .5 3.9 1.5c1 1.1 1.5 2.4 1.5 4.2v11.1h-2.7v-10.8c0-1.2-0.3-2.1-0.9-2.7c-0.7-0.7-1.5-1-2.6-1c-1.1 0-2 .3-2.7 1c-0.6 .7-0.9 1.7-0.9 3v10.5Zm22.8 .3c-1.8 0-3.1-0.4-4.1-1.3c-1-0.9-1.5-2.1-1.5-3.7c0-1.5 .5-2.7 1.5-3.6c1-0.9 2.3-1.3 4-1.3h5.1v-1.7c0-1-0.3-1.7-0.9-2.3c-0.6-0.6-1.5-0.8-2.5-0.8c-1 0-1.8 .2-2.5 .6c-0.6 .4-1 1-1.2 1.7h-2.7c.1-1 .4-1.8 1-2.5c.6-0.7 1.3-1.2 2.2-1.6c1-0.4 2-0.6 3.2-0.6c1.9 0 3.4 .5 4.5 1.5c1.1 1 1.6 2.3 1.6 4v11.3h-2.6v-3.2h-0.1c-0.1 1.1-0.6 1.9-1.5 2.6c-0.9 .6-2.1 .9-3.5 .9Zm.6-2.1c1.3 0 2.4-0.4 3.2-1c.8-0.6 1.2-1.5 1.2-2.5v-2.3h-5c-0.9 0-1.6 .3-2.2 .8c-0.5 .5-0.7 1.1-0.7 2c0 .9 .3 1.6 .9 2.2c.6 .5 1.5 .8 2.6 .8Zm20.6 1.8c-1.4 0-2.4-0.4-3.3-1.2c-0.8-0.8-1.2-1.8-1.2-3.2v-9.6h-4.6v-2.5h4.6v-4.7h2.7v4.7h6.6v2.5h-6.6v9.6c0 .6 .2 1.1 .5 1.4c.3 .4 .8 .5 1.3 .5h4.5v2.5Zm28.5 0v-16.5h2.7v3.1c.1-1.1 .6-1.9 1.4-2.5c.7-0.6 1.7-0.9 3-0.9c1.7 0 3.1 .5 4.1 1.6c1 1.1 1.5 2.6 1.5 4.5v1.1h-2.7v-0.9c0-1.3-0.3-2.2-0.9-2.9c-0.7-0.7-1.6-1.1-2.8-1.1c-2.4 0-3.6 1.4-3.6 4v10.5Zm23.7 .3c-1.9 0-3.4-0.6-4.6-1.7c-1.1-1.1-1.7-2.7-1.7-4.6v-10.5h2.7v10.5c0 1.2 .3 2.2 1 2.9c.6 .7 1.5 1 2.6 1c1.1 0 2-0.3 2.6-1c.7-0.7 1-1.7 1-2.9v-10.5h2.7v10.5c0 1.9-0.6 3.5-1.7 4.6c-1.2 1.1-2.7 1.7-4.6 1.7Zm11.8-0.3v-16.5h2.7v3.1c.1-1.1 .6-1.9 1.3-2.5c.8-0.6 1.8-0.9 3.1-0.9c1.6 0 2.9 .5 3.9 1.5c1 1 1.5 2.4 1.5 4.2v11.1h-2.7v-10.8c0-1.2-0.3-2.1-0.9-2.7c-0.7-0.6-1.5-1-2.6-1c-1.1 0-2 .4-2.7 1.1c-0.6 .6-0.9 1.6-0.9 2.9v10.5Zm40.8 .3c-1.8 0-3.2-0.4-4.2-1.3c-1-0.9-1.5-2.1-1.5-3.7c0-1.5 .5-2.7 1.5-3.6c1-0.9 2.4-1.3 4.1-1.3h5.1v-1.7c0-1-0.3-1.7-0.9-2.3c-0.6-0.6-1.5-0.8-2.6-0.8c-0.9 0-1.7 .2-2.4 .6c-0.6 .4-1.1 1-1.2 1.6h-2.7c.1-0.9 .4-1.7 1-2.4c.6-0.7 1.3-1.2 2.2-1.6c1-0.4 2-0.6 3.1-0.6c1.9 0 3.4 .5 4.5 1.5c1.1 .9 1.7 2.3 1.7 4v11.3h-2.6v-3.2h-0.1c-0.1 1.1-0.6 1.9-1.5 2.6c-0.9 .6-2.1 .9-3.5 .9Zm.6-2.1c1.3 0 2.3-0.4 3.2-1c.8-0.6 1.2-1.5 1.2-2.5v-2.3h-5c-0.9 0-1.7 .3-2.2 .8c-0.5 .5-0.8 1.1-0.8 2c0 .9 .4 1.6 1 2.2c.6 .5 1.5 .8 2.6 .8Zm12.6 1.8v-16.5h2.7v3.1c.1-1.1 .6-1.9 1.3-2.5c.8-0.6 1.8-0.9 3.1-0.9c1.6 0 2.9 .5 3.9 1.5c1 1 1.5 2.4 1.5 4.2v11.1h-2.7v-10.8c0-1.2-0.3-2.1-0.9-2.7c-0.7-0.6-1.5-1-2.6-1c-1.1 0-2 .4-2.7 1.1c-0.6 .6-0.9 1.6-0.9 2.9v10.5Zm20.6 5.4l2.4-6.4l-6.3-15.5h2.9l4.1 10.2c.1 .3 .2 .7 .4 1.2c.1 .5 .2 .9 .3 1.2c.1-0.3 .2-0.7 .3-1.2c.1-0.5 .3-0.9 .4-1.2l3.8-10.2h2.8l-8.3 21.9Zm16.3-5.4l-2.7-16.5h2.4l1.6 11.7c.1 .4 .2 .9 .2 1.4c.1 .5 .1 1 .1 1.3h.2c0-0.3 0-0.8 .1-1.3c.1-0.5 .1-1 .2-1.4l1.8-11.7h2.7l1.9 11.7c.1 .4 .1 .9 .2 1.4c.1 .5 .1 1 .1 1.3h.2c0-0.3 .1-0.8 .1-1.3c.1-0.5 .2-1 .2-1.4l1.7-11.7h2.2l-2.7 16.5h-3l-1.8-11.4c0-0.6-0.1-1.1-0.2-1.7c-0.1-0.5-0.1-1-0.2-1.3h-0.1c0 .3-0.1 .8-0.1 1.3c-0.1 .6-0.2 1.1-0.3 1.7l-1.8 11.4Zm17.1 0v-21.9h2.7v8.5c.1-1.1 .6-1.9 1.3-2.5c.8-0.6 1.8-0.9 3.1-0.9c1.6 0 2.9 .5 3.9 1.5c1 1 1.5 2.4 1.5 4.2v11.1h-2.7v-10.8c0-1.2-0.3-2.1-0.9-2.7c-0.7-0.7-1.5-1-2.6-1c-1.1 0-2 .3-2.7 1c-0.6 .7-0.9 1.7-0.9 3v10.5Zm24.2 .3c-1.3 0-2.4-0.3-3.4-0.8c-1-0.5-1.7-1.2-2.3-2.2c-0.5-0.9-0.8-2-0.8-3.3v-4.5c0-1.3 .3-2.4 .8-3.3c.6-1 1.3-1.7 2.3-2.2c1-0.5 2.1-0.8 3.4-0.8c1.3 0 2.4 .3 3.4 .8c1 .5 1.7 1.2 2.3 2.2c.5 .9 .7 2 .7 3.3v2.9h-10.2v1.6c0 1.3 .3 2.3 1 3c.6 .7 1.6 1 2.8 1c1 0 1.9-0.2 2.5-0.5c.7-0.4 1-1 1.2-1.7h2.7c-0.2 1.4-0.9 2.5-2 3.3c-1.2 .8-2.7 1.2-4.4 1.2Zm-3.8-9.9h7.6v-0.9c0-1.3-0.3-2.3-1-3c-0.6-0.7-1.6-1.1-2.8-1.1c-1.2 0-2.2 .4-2.8 1.1c-0.7 .7-1 1.7-1 3Zm16.1 9.6v-16.5h2.7v3.1c.1-1.1 .6-1.9 1.3-2.5c.8-0.6 1.8-0.9 3.1-0.9c1.7 0 3.1 .5 4.1 1.6c1 1.1 1.5 2.6 1.5 4.5v1.1h-2.7v-0.9c0-1.3-0.3-2.2-0.9-2.9c-0.7-0.7-1.6-1.1-2.8-1.1c-2.4 0-3.6 1.4-3.6 4v10.5Zm23.7 .3c-1.3 0-2.4-0.3-3.4-0.8c-1-0.5-1.7-1.2-2.3-2.2c-0.5-0.9-0.8-2-0.8-3.3v-4.5c0-1.3 .3-2.4 .8-3.3c.6-1 1.3-1.7 2.3-2.2c1-0.5 2.1-0.8 3.4-0.8c1.3 0 2.4 .3 3.4 .8c1 .5 1.7 1.2 2.3 2.2c.5 .9 .7 2 .7 3.3v2.9h-10.2v1.6c0 1.3 .3 2.3 1 3c.6 .7 1.6 1 2.8 1c1 0 1.9-0.2 2.5-0.5c.7-0.4 1-1 1.2-1.7h2.7c-0.2 1.4-0.9 2.5-2 3.3c-1.2 .8-2.7 1.2-4.4 1.2Zm-3.8-9.9h7.6v-0.9c0-1.3-0.3-2.3-1-3c-0.6-0.7-1.6-1.1-2.8-1.1c-1.2 0-2.2 .4-2.8 1.1c-0.7 .7-1 1.7-1 3Z"/><g transform="translate(764,-14.5) scale(.431111,.440249) translate(-13.9,56.6)"><path fill="#e0e0e0" stroke="#e0e0e0" stroke-linejoin="round" d="M-50.7 4h278.2c10 0 9 0 9 0l41 40c0 0 1 0-9 0h-358c-9.9 0-9 0-9 0l38.8-40c0 0-0.9 0 9 0Z" stroke-width="10" transform="translate(225,339.9) scale(0,1) translate(-89.5,-24)" style="animation: 10s linear infinite both a0_t;"/><rect width="83" height="22" stroke="#fb422d" fill="#e0e0e0" rx="2" stroke-width="0" transform="translate(225,348.9) scale(0,1) translate(-41.5,-11)" style="animation: 10s linear infinite both a1_t;"/><g style="animation: 10s linear infinite both a3_t;"><rect width="400" height="334" stroke="#e0e0e0" fill="none" stroke-width="30" stroke-miterlimit="1" rx="10" opacity="0" transform="translate(225,198.9) rotate(-90) translate(-200,-167)" style="animation: 10s linear infinite both a2_t, 10s linear infinite both a2_o, 10s linear infinite both a2_sw, 10s linear infinite both a2_w, 10s linear infinite both a2_h;"/></g><use width="168.5" height="302" xlink:href="#Symbol-2" opacity="0" transform="translate(227.8,199.9) translate(-84.2,-151)" style="animation: 10s linear infinite both a4_t, 10s linear infinite both a4_w, 10s linear infinite both a4_h;"/><rect width="16" height="8" stroke="#fb422d" fill="#e0e0e0" stroke-width="0" rx="2" opacity="0" transform="translate(225,328.9) translate(-8,-4)" style="animation: 10s linear infinite both a5_t, 10s linear infinite both a5_o, 10s linear infinite both a5_w;"/><rect width="16" height="8" stroke="#fb422d" fill="#e0e0e0" stroke-width="0" rx="2" opacity="0" transform="translate(225,328.9) translate(-8,-4)" style="animation: 10s linear infinite both a6_t, 10s linear infinite both a6_o, 10s linear infinite both a6_w;"/><g transform="translate(225,198.9) scale(1.5,1.5) translate(-42.3,-93)" style="animation: 10s linear infinite both a7_t;"><g transform="translate(42.3,93) scale(5.352,6.9) translate(-16,60)"><g transform="translate(-34.9,-125.8)"><g transform="translate(24,5)"><path d="M33 46.5c0 3.1-0.8 5.5-2.2 7.4c-1.4 1.9-3.3 3.6-5.4 5.4c-2.2 1.7-4.5 3.6-6.3 6.2c-1.8 2.5-3.1 5.5-3.1 9.5h4.7c0-3.1 .9-5.4 2.2-7.3c1.4-1.9 3.3-3.5 5.5-5.3c2.1-1.8 4.4-3.7 6.2-6.2c1.8-2.6 3.1-5.6 3.1-9.7Z" fill="#e96020"/><path d="M20.4 70.7c-0.6 0-1 .5-1 1.1c0 .7 .4 1.2 1 1.2h12.9c.7 0 1.2-0.5 1.2-1.2c0-0.6-0.5-1.1-1.2-1.1Z" fill="#2d323b"/><path d="M21.8 66.6c-0.7 0-1 .6-1 1.2c0 .6 .3 1 .9 1h10.3c.6 0 1.1-0.5 1.1-1.1c0-0.6-0.5-1.1-1.1-1.1Z" fill="#2d323b"/><path d="M21.8 53c-0.7 0-1.2 .5-1.1 1.1c0 .6 .4 1.1 1.1 1.1h10.2c.6 0 .9-0.6 .9-1.2c0-0.6-0.3-1-0.9-1Z" fill="#2d323b"/><path d="M20.4 48.8c-0.6 0-1.2 .5-1.2 1.1c0 .7 .6 1.2 1.2 1.2h12.9c.7 0 1-0.5 1-1.1c0-0.7-0.3-1.2-1-1.2Z" fill="#2d323b"/><path d="M16 46.5c0 4.1 1.3 7.1 3.1 9.7c1.8 2.5 4.1 4.4 6.3 6.2c2.1 1.8 4 3.6 5.4 5.5c1.4 1.9 2.2 4 2.2 7.1h4.7c0-4-1.3-7.1-3.1-9.6c-1.8-2.5-4.1-4.4-6.2-6.2c-2.2-1.8-4.1-3.4-5.5-5.3c-1.3-1.9-2.2-4.3-2.2-7.4Z" fill="#00a8d6"/></g></g></g></g></g></svg> \ No newline at end of file diff --git a/examples/subsecond_hot_patch/assets/main.css b/examples/subsecond_hot_patch/assets/main.css new file mode 100644 index 0000000000..90c0fc1c09 --- /dev/null +++ b/examples/subsecond_hot_patch/assets/main.css @@ -0,0 +1,46 @@ +/* App-wide styling */ +body { + background-color: #0f1116; + color: #ffffff; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + margin: 20px; +} + +#hero { + margin: 0; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +#links { + width: 400px; + text-align: left; + font-size: x-large; + color: white; + display: flex; + flex-direction: column; +} + +#links a { + color: white; + text-decoration: none; + margin-top: 20px; + margin: 10px 0px; + border: white 1px solid; + border-radius: 5px; + padding: 10px; +} + +#links a:hover { + background-color: #1f1f1f; + cursor: pointer; +} + +#header { + max-width: 1200px; +} + + + diff --git a/examples/subsecond_hot_patch/src/main.rs b/examples/subsecond_hot_patch/src/main.rs new file mode 100644 index 0000000000..0b75d4f6a2 --- /dev/null +++ b/examples/subsecond_hot_patch/src/main.rs @@ -0,0 +1,44 @@ +use leptos::{prelude::*, subsecond::connect_to_hot_patch_messages}; +use leptos_router::{ + components::{Route, Router, Routes}, + path, +}; + +fn main() { + // connect to DX CLI and patch the WASM binary whenever we receive a message + connect_to_hot_patch_messages(); + + // wrapping App here in a closure so we can hot-reload it, because we only do that + // for reactive views right now. changing anything will re-run App and update the view + mount_to_body(|| App); +} + +#[component] +fn App() -> impl IntoView { + view! { + <nav> + <a href="/">"Home"</a> + <a href="/about">"About"</a> + </nav> + <Router> + <Routes fallback=|| "Not found"> + <Route path=path!("/") view=HomePage/> + <Route path=path!("/about") view=About/> + </Routes> + </Router> + } +} + +#[component] +fn HomePage() -> impl IntoView { + view! { + <h1>"Home Page"</h1> + } +} + +#[component] +fn About() -> impl IntoView { + view! { + <h1>"About"</h1> + } +} diff --git a/leptos/Cargo.toml b/leptos/Cargo.toml index 1661e25b6b..2550394020 100644 --- a/leptos/Cargo.toml +++ b/leptos/Cargo.toml @@ -58,6 +58,9 @@ slotmap = { workspace = true, default-features = true } futures = { workspace = true, default-features = true } send_wrapper = { workspace = true, default-features = true } wasm_split_helpers.workspace = 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 } [features] hydration = [ @@ -102,6 +105,16 @@ trace-component-props = [ ] delegation = ["tachys/delegation"] islands-router = ["tachys/mark_branches"] +subsecond = [ + "reactive_graph/subsecond", + "dep:subsecond", + "dep:dioxus-cli-config", + "dep:dioxus-devtools", + "web-sys/Location", + "web-sys/MessageEvent", + "web-sys/WebSocket", + "web-sys/Window", +] [dev-dependencies] tokio = { features = [ diff --git a/leptos/src/lib.rs b/leptos/src/lib.rs index d287df6f75..b6f6ae8c55 100644 --- a/leptos/src/lib.rs +++ b/leptos/src/lib.rs @@ -1,5 +1,4 @@ #![deny(missing_docs)] -#![forbid(unsafe_code)] //! # About Leptos //! @@ -306,6 +305,10 @@ pub use tachys::mathml as math; #[doc(inline)] pub use tachys::svg; +#[cfg(feature = "subsecond")] +/// Utilities for using binary hot-patching with [`subsecond`]. +pub mod subsecond; + /// Utilities for simple isomorphic logging to the console or terminal. pub mod logging { pub use leptos_dom::{debug_warn, error, log, warn}; diff --git a/leptos/src/subsecond.rs b/leptos/src/subsecond.rs new file mode 100644 index 0000000000..6a04c64f07 --- /dev/null +++ b/leptos/src/subsecond.rs @@ -0,0 +1,62 @@ +use dioxus_devtools::DevserverMsg; +use wasm_bindgen::{prelude::Closure, JsCast}; +use web_sys::{js_sys::JsString, MessageEvent, WebSocket}; + +/// Sets up a websocket connect to the `dx` CLI, waiting for incoming hot-patching messages +/// and patching the WASM binary appropriately. +// +// Note: This is a stripped-down version of Dioxus's `make_ws` from `dioxus_web` +// It's essentially copy-pasted here because it's not pub there. +// Would love to just take a dependency on that to be able to use it and deduplicate. +// +// https://github.com/DioxusLabs/dioxus/blob/main/packages/web/src/devtools.rs#L36 +pub fn connect_to_hot_patch_messages() { + // Get the location of the devserver, using the current location plus the /_dioxus path + // The idea here being that the devserver is always located on the /_dioxus behind a proxy + let location = web_sys::window().unwrap().location(); + let url = format!( + "{protocol}//{host}/_dioxus?build_id={build_id}", + protocol = match location.protocol().unwrap() { + prot if prot == "https:" => "wss:", + _ => "ws:", + }, + host = location.host().unwrap(), + build_id = dioxus_cli_config::build_id(), + ); + + let ws = WebSocket::new(&url).unwrap(); + + ws.set_onmessage(Some( + Closure::<dyn FnMut(MessageEvent)>::new(move |e: MessageEvent| { + let Ok(text) = e.data().dyn_into::<JsString>() else { + return; + }; + + // The devserver messages have some &'static strs in them, so we need to leak the source string + let string: String = text.into(); + let string = Box::leak(string.into_boxed_str()); + + if let Ok(DevserverMsg::HotReload(msg)) = + serde_json::from_str::<DevserverMsg>(string) + { + if let Some(jump_table) = msg.jump_table.as_ref().cloned() { + if msg.for_build_id == Some(dioxus_cli_config::build_id()) { + let our_pid = if cfg!(target_family = "wasm") { + None + } else { + Some(std::process::id()) + }; + + if msg.for_pid == our_pid { + unsafe { subsecond::apply_patch(jump_table) } + .unwrap(); + } + } + } + } + }) + .into_js_value() + .as_ref() + .unchecked_ref(), + )); +} diff --git a/leptos_dom/src/helpers.rs b/leptos_dom/src/helpers.rs index ceefc565e5..9d878bd7d3 100644 --- a/leptos_dom/src/helpers.rs +++ b/leptos_dom/src/helpers.rs @@ -463,7 +463,7 @@ pub fn set_interval_with_handle( #[inline(never)] fn si( - cb: Box<dyn Fn()>, + cb: Box<dyn FnMut()>, duration: Duration, ) -> Result<IntervalHandle, JsValue> { let cb = Closure::wrap(cb).into_js_value(); diff --git a/reactive_graph/Cargo.toml b/reactive_graph/Cargo.toml index d63ca8aa5f..e32e19cd51 100644 --- a/reactive_graph/Cargo.toml +++ b/reactive_graph/Cargo.toml @@ -27,6 +27,7 @@ async-lock = { workspace = true, default-features = true } send_wrapper = { features = [ "futures", ], workspace = true, default-features = true } +subsecond = { workspace = true, default-features = true, optional = true } indexmap = { workspace = true, default-features = true } [target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies] @@ -51,6 +52,7 @@ hydration = ["dep:hydration_context"] effects = [ ] # whether to run effects: should be disabled for something like server rendering sandboxed-arenas = [] +subsecond = ["dep:subsecond"] [package.metadata.docs.rs] all-features = true diff --git a/reactive_graph/src/effect/render_effect.rs b/reactive_graph/src/effect/render_effect.rs index 53892828bc..557aea1861 100644 --- a/reactive_graph/src/effect/render_effect.rs +++ b/reactive_graph/src/effect/render_effect.rs @@ -9,6 +9,8 @@ use crate::{ }; use futures::StreamExt; use or_poisoned::OrPoisoned; +#[cfg(feature = "subsecond")] +use std::sync::Mutex; use std::{ fmt::Debug, future::{Future, IntoFuture}, @@ -49,13 +51,39 @@ impl<T> Debug for RenderEffect<T> { } } +#[cfg(feature = "subsecond")] +type CurrentHotPtr = Box<dyn Fn() -> Option<subsecond::HotFnPtr> + Send + Sync>; + impl<T> RenderEffect<T> where T: 'static, { /// Creates a new render effect, which immediately runs `fun`. pub fn new(fun: impl FnMut(Option<T>) -> T + 'static) -> Self { - Self::new_with_value_erased(Box::new(fun), None) + #[cfg(feature = "subsecond")] + let (hot_fn_ptr, fun) = { + let fun = Arc::new(Mutex::new(subsecond::HotFn::current(fun))); + ( + { + let fun = Arc::downgrade(&fun); + let wrapped = send_wrapper::SendWrapper::new(move || { + fun.upgrade() + .map(|n| n.lock().or_poisoned().ptr_address()) + }); + // it's not redundant, it's due to the SendWrapper deref + #[allow(clippy::redundant_closure)] + Box::new(move || wrapped()) + }, + move |prev| fun.lock().or_poisoned().call((prev,)), + ) + }; + + Self::new_with_value_erased( + Box::new(fun), + None, + #[cfg(feature = "subsecond")] + hot_fn_ptr, + ) } /// Creates a new render effect with an initial value. @@ -63,7 +91,30 @@ where fun: impl FnMut(Option<T>) -> T + 'static, initial_value: Option<T>, ) -> Self { - Self::new_with_value_erased(Box::new(fun), initial_value) + #[cfg(feature = "subsecond")] + let (hot_fn_ptr, fun) = { + let fun = Arc::new(Mutex::new(subsecond::HotFn::current(fun))); + ( + { + let fun = Arc::downgrade(&fun); + let wrapped = send_wrapper::SendWrapper::new(move || { + fun.upgrade() + .map(|n| n.lock().or_poisoned().ptr_address()) + }); + // it's not redundant, it's due to the SendWrapper deref + #[allow(clippy::redundant_closure)] + Box::new(move || wrapped()) + }, + move |prev| fun.lock().or_poisoned().call((prev,)), + ) + }; + + Self::new_with_value_erased( + Box::new(fun), + initial_value, + #[cfg(feature = "subsecond")] + hot_fn_ptr, + ) } /// Creates a new render effect, which immediately runs `fun`. @@ -71,6 +122,11 @@ where fun: impl FnMut(Option<T>) -> T + 'static, value: impl IntoFuture<Output = T> + 'static, ) -> Self { + #[cfg(feature = "subsecond")] + let mut fun = subsecond::HotFn::current(fun); + #[cfg(feature = "subsecond")] + let fun = move |prev| fun.call((prev,)); + Self::new_with_async_value_erased( Box::new(fun), Box::pin(value.into_future()), @@ -79,8 +135,13 @@ where } fn new_with_value_erased( - mut fun: Box<dyn FnMut(Option<T>) -> T + 'static>, + #[allow(unused_mut)] mut fun: Box<dyn FnMut(Option<T>) -> T + 'static>, initial_value: Option<T>, + // this argument can be used to invalidate individual effects in the future + // in present experiments, I have found that it is not actually granular enough to make a difference + #[allow(unused)] + #[cfg(feature = "subsecond")] + hot_fn_ptr: CurrentHotPtr, ) -> Self { // codegen optimisation: fn prep() -> (Owner, Arc<RwLock<EffectInner>>, crate::channel::Receiver) @@ -104,12 +165,56 @@ where let _ = initial_value; let _ = owner; let _ = &mut rx; - let _ = &mut fun; + let _ = fun; } #[cfg(feature = "effects")] { let subscriber = inner.to_any_subscriber(); + + #[cfg(all(feature = "subsecond", debug_assertions))] + let mut fun = { + use crate::graph::ReactiveNode; + use rustc_hash::FxHashMap; + use std::sync::{Arc, LazyLock, Mutex}; + use subsecond::HotFnPtr; + + static HOT_RELOAD_SUBSCRIBERS: LazyLock< + Mutex<FxHashMap<AnySubscriber, (HotFnPtr, CurrentHotPtr)>>, + > = LazyLock::new(|| { + subsecond::register_handler(Arc::new(|| { + HOT_RELOAD_SUBSCRIBERS.lock().or_poisoned().retain( + |subscriber, (prev_ptr, hot_fn_ptr)| { + match hot_fn_ptr() { + None => false, + Some(curr_hot_ptr) => { + if curr_hot_ptr != *prev_ptr { + crate::log_warning(format_args!( + "{prev_ptr:?} <> \ + {curr_hot_ptr:?}", + )); + *prev_ptr = curr_hot_ptr; + + subscriber.mark_dirty(); + } + true + } + } + }, + ); + })); + Default::default() + }); + + let mut fun = subsecond::HotFn::current(fun); + let initial_ptr = hot_fn_ptr().unwrap(); + HOT_RELOAD_SUBSCRIBERS + .lock() + .or_poisoned() + .insert(subscriber.clone(), (initial_ptr, hot_fn_ptr)); + move |prev| fun.call((prev,)) + }; + *value.write().or_poisoned() = Some( owner.with(|| subscriber.with_observer(|| fun(initial_value))), ); @@ -230,6 +335,11 @@ where pub fn new_isomorphic( fun: impl FnMut(Option<T>) -> T + Send + Sync + 'static, ) -> Self { + #[cfg(feature = "subsecond")] + let mut fun = subsecond::HotFn::current(fun); + #[cfg(feature = "subsecond")] + let fun = move |prev| fun.call((prev,)); + fn erased<T: Send + Sync + 'static>( mut fun: Box<dyn FnMut(Option<T>) -> T + Send + Sync + 'static>, ) -> RenderEffect<T> { From b750f57dc39bb413615acb0692b8e242a4604936 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 20 Sep 2025 11:40:41 -0400 Subject: [PATCH 33/91] chore(deps): bump actions/setup-node from 4 to 5 (#4283) Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 5. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-node dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/run-cargo-make-task.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-cargo-make-task.yml b/.github/workflows/run-cargo-make-task.yml index 63e00a11a7..473dee7e4f 100644 --- a/.github/workflows/run-cargo-make-task.yml +++ b/.github/workflows/run-cargo-make-task.yml @@ -88,7 +88,7 @@ jobs: run: trunk --version - name: Install Node.js if: contains(inputs.directory, 'examples') - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: 20 - uses: pnpm/action-setup@v4 From 8c6f3f62aa4c6d536be46938dfae6142086e91e6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 20 Sep 2025 11:40:53 -0400 Subject: [PATCH 34/91] chore(deps): bump tj-actions/changed-files from 46 to 47 (#4297) Bumps [tj-actions/changed-files](https://github.com/tj-actions/changed-files) from 46 to 47. - [Release notes](https://github.com/tj-actions/changed-files/releases) - [Changelog](https://github.com/tj-actions/changed-files/blob/main/HISTORY.md) - [Commits](https://github.com/tj-actions/changed-files/compare/v46...v47) --- updated-dependencies: - dependency-name: tj-actions/changed-files dependency-version: '47' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/get-example-changed.yml | 2 +- .github/workflows/get-leptos-changed.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/get-example-changed.yml b/.github/workflows/get-example-changed.yml index 6bdc9fe38f..93bb4fc1b9 100644 --- a/.github/workflows/get-example-changed.yml +++ b/.github/workflows/get-example-changed.yml @@ -24,7 +24,7 @@ jobs: fetch-depth: 0 - name: Get example files that changed id: changed-files - uses: tj-actions/changed-files@v46 + uses: tj-actions/changed-files@v47 with: files: | examples/** diff --git a/.github/workflows/get-leptos-changed.yml b/.github/workflows/get-leptos-changed.yml index ada812a5ae..19efebb196 100644 --- a/.github/workflows/get-leptos-changed.yml +++ b/.github/workflows/get-leptos-changed.yml @@ -18,7 +18,7 @@ jobs: fetch-depth: 0 - name: Get source files that changed id: changed-source - uses: tj-actions/changed-files@v46 + uses: tj-actions/changed-files@v47 with: files_ignore: | .*/**/* From 7358e89c6f2ce013ba12053cc43c9055c3d5495a Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Sun, 21 Sep 2025 14:38:13 -0400 Subject: [PATCH 35/91] chore: specify Tailwind version in Trunk.toml (closes #4315) (#4317) --- examples/tailwind_csr/Trunk.toml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 examples/tailwind_csr/Trunk.toml diff --git a/examples/tailwind_csr/Trunk.toml b/examples/tailwind_csr/Trunk.toml new file mode 100644 index 0000000000..0878e8fd52 --- /dev/null +++ b/examples/tailwind_csr/Trunk.toml @@ -0,0 +1,3 @@ +[tools] +tailwindcss = "4.1.13" + From 531cb716a62978f0a628872c8318827181b10f02 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Tue, 23 Sep 2025 10:59:04 -0400 Subject: [PATCH 36/91] fix: remove event listeners correctly when dropping handles (closes #4313) (#4314) --- tachys/src/html/event.rs | 4 +++- tachys/src/reactive_graph/bind.rs | 4 +++- tachys/src/renderer/dom.rs | 30 +++++++++++++++++------------- tachys/src/renderer/mod.rs | 29 +++++++++++++++++++++++------ 4 files changed, 46 insertions(+), 21 deletions(-) diff --git a/tachys/src/html/event.rs b/tachys/src/html/event.rs index 4235069be0..8083b5e9f7 100644 --- a/tachys/src/html/event.rs +++ b/tachys/src/html/event.rs @@ -323,7 +323,9 @@ where fn rebuild(self, state: &mut Self::State) { let (el, prev_cleanup) = state; if let Some(prev) = prev_cleanup.take() { - (prev.into_inner())(el); + if let Some(remove) = prev.into_inner() { + remove(); + } } *prev_cleanup = Some(if E::CAPTURE { self.attach_capture(el) diff --git a/tachys/src/reactive_graph/bind.rs b/tachys/src/reactive_graph/bind.rs index 1cee2b6fa3..7033ce9d1d 100644 --- a/tachys/src/reactive_graph/bind.rs +++ b/tachys/src/reactive_graph/bind.rs @@ -258,7 +258,9 @@ where prop(self.key(), signal).rebuild(attr_state); if let Some(prev) = prev_cleanup.take() { - (prev.into_inner())(el); + if let Some(remove) = prev.into_inner() { + remove(); + } } *prev_cleanup = Some(self.attach(el)); } diff --git a/tachys/src/renderer/dom.rs b/tachys/src/renderer/dom.rs index c245875114..37d8885e50 100644 --- a/tachys/src/renderer/dom.rs +++ b/tachys/src/renderer/dom.rs @@ -296,19 +296,20 @@ impl Dom { // return the remover RemoveEventHandler::new({ let name = name.to_owned(); + let el = el.clone(); // safe to construct this here, because it will only run in the browser // so it will always be accessed or dropped from the main thread - let cb = send_wrapper::SendWrapper::new(cb); - move |el: &Element| { + let cb = send_wrapper::SendWrapper::new(move || { or_debug!( el.remove_event_listener_with_callback( intern(&name), cb.as_ref().unchecked_ref() ), - el, + &el, "removeEventListener" ) - } + }); + move || cb() }) } @@ -334,20 +335,21 @@ impl Dom { // return the remover RemoveEventHandler::new({ let name = name.to_owned(); + let el = el.clone(); // safe to construct this here, because it will only run in the browser // so it will always be accessed or dropped from the main thread - let cb = send_wrapper::SendWrapper::new(cb); - move |el: &Element| { + let cb = send_wrapper::SendWrapper::new(move || { or_debug!( el.remove_event_listener_with_callback_and_bool( intern(&name), cb.as_ref().unchecked_ref(), true ), - el, + &el, "removeEventListener" ) - } + }); + move || cb() }) } @@ -448,17 +450,19 @@ impl Dom { // return the remover RemoveEventHandler::new({ let key = key.to_owned(); + let el = el.clone(); // safe to construct this here, because it will only run in the browser // so it will always be accessed or dropped from the main thread - let cb = send_wrapper::SendWrapper::new(cb); - move |el: &Element| { - drop(cb.take()); + let el_cb = send_wrapper::SendWrapper::new((el, cb)); + move || { + let (el, cb) = el_cb.take(); + drop(cb); or_debug!( js_sys::Reflect::delete_property( - el, + &el, &JsValue::from_str(&key) ), - el, + &el, "delete property" ); } diff --git a/tachys/src/renderer/mod.rs b/tachys/src/renderer/mod.rs index cbb515db59..da488116c0 100644 --- a/tachys/src/renderer/mod.rs +++ b/tachys/src/renderer/mod.rs @@ -1,5 +1,5 @@ use crate::view::{Mountable, ToTemplate}; -use std::{borrow::Cow, fmt::Debug}; +use std::{borrow::Cow, fmt::Debug, marker::PhantomData}; use wasm_bindgen::JsValue; /// A DOM renderer. @@ -120,16 +120,33 @@ pub trait Renderer: Send + Sized + Debug + 'static { should store it in some other data structure to clean it up \ later to avoid dropping it immediately, or leak it with \ std::mem::forget() to never drop it."] -pub struct RemoveEventHandler<T>(Box<dyn FnOnce(&T) + Send + Sync>); +#[allow(clippy::type_complexity)] +pub struct RemoveEventHandler<T>( + Option<Box<dyn FnOnce() + Send + Sync>>, + // only here to keep the generic, removing which would be a breaking change + // TODO remove generic in 0.9 + PhantomData<fn() -> T>, +); impl<T> RemoveEventHandler<T> { /// Creates a new container with a function that will be called when it is dropped. - pub(crate) fn new(remove: impl FnOnce(&T) + Send + Sync + 'static) -> Self { - Self(Box::new(remove)) + pub(crate) fn new(remove: impl FnOnce() + Send + Sync + 'static) -> Self { + Self(Some(Box::new(remove)), PhantomData) } - pub(crate) fn into_inner(self) -> Box<dyn FnOnce(&T) + Send + Sync> { - self.0 + #[allow(clippy::type_complexity)] + pub(crate) fn into_inner( + mut self, + ) -> Option<Box<dyn FnOnce() + Send + Sync>> { + self.0.take() + } +} + +impl<T> Drop for RemoveEventHandler<T> { + fn drop(&mut self) { + if let Some(cb) = self.0.take() { + cb() + } } } From b166957d343ab2c51b0eb81fb8ce99816637e248 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Wed, 24 Sep 2025 17:00:55 -0400 Subject: [PATCH 37/91] fix: preload correct `__wasm_split.*.js` file (closes #4322) (#4327) --- integrations/utils/src/lib.rs | 5 ++-- leptos/src/hydration/mod.rs | 46 ++++++++++++++++++++++++++++++++--- leptos/src/lib.rs | 5 ++-- 3 files changed, 49 insertions(+), 7 deletions(-) diff --git a/integrations/utils/src/lib.rs b/integrations/utils/src/lib.rs index 979ffe9be5..b4438eed98 100644 --- a/integrations/utils/src/lib.rs +++ b/integrations/utils/src/lib.rs @@ -68,7 +68,8 @@ pub trait ExtendResponse: Sized { let nonce = use_nonce().map(|n| n.to_string()).unwrap_or_default(); if let Some(manifest) = use_context::<WasmSplitManifest>() { - let (pkg_path, manifest) = &*manifest.0.read_value(); + let (pkg_path, manifest, wasm_split_file) = + &*manifest.0.read_value(); let prefetches = prefetches.0.read_value(); let all_prefetches = prefetches.iter().flat_map(|key| { @@ -90,7 +91,7 @@ pub trait ExtendResponse: Sized { .to_html(); } _ = view! { - <Link rel="modulepreload" href=format!("{pkg_path}/__wasm_split.js") crossorigin=nonce/> + <Link rel="modulepreload" href=format!("{pkg_path}/{wasm_split_file}") crossorigin=nonce/> } .to_html(); } diff --git a/leptos/src/hydration/mod.rs b/leptos/src/hydration/mod.rs index 60721459fb..ba9f63e491 100644 --- a/leptos/src/hydration/mod.rs +++ b/leptos/src/hydration/mod.rs @@ -65,16 +65,56 @@ pub fn HydrationScripts( if let Some(splits) = SPLIT_MANIFEST.get_or_init(|| { let root = root.clone().unwrap_or_default(); + let (wasm_split_js, wasm_split_manifest) = if options.hash_files { + let hash_path = std::env::current_exe() + .map(|path| { + path.parent().map(|p| p.to_path_buf()).unwrap_or_default() + }) + .unwrap_or_default() + .join(options.hash_file.as_ref()); + let hashes = std::fs::read_to_string(&hash_path) + .expect("failed to read hash file"); + + let mut split = + "__wasm_split.______________________.js".to_string(); + let mut manifest = "__wasm_split_manifest.json".to_string(); + for line in hashes.lines() { + let line = line.trim(); + if !line.is_empty() { + if let Some((file, hash)) = line.split_once(':') { + if file == "manifest" { + manifest.clear(); + manifest.push_str("__wasm_split_manifest."); + manifest.push_str(hash.trim()); + manifest.push_str(".json"); + } + if file == "split" { + split.clear(); + split.push_str("__wasm_split."); + split.push_str(hash.trim()); + split.push_str(".js"); + } + } + } + } + (split, manifest) + } else { + ( + "__wasm_split.______________________.js".to_string(), + "__wasm_split_manifest.json".to_string(), + ) + }; + let site_dir = &options.site_root; let pkg_dir = &options.site_pkg_dir; let path = PathBuf::from(site_dir.to_string()); - let path = path - .join(pkg_dir.to_string()) - .join("__wasm_split_manifest.json"); + let path = path.join(pkg_dir.to_string()).join(wasm_split_manifest); let file = std::fs::read_to_string(path).ok()?; + let manifest = WasmSplitManifest(ArcStoredValue::new(( format!("{root}/{pkg_dir}"), serde_json::from_str(&file).expect("could not read manifest file"), + wasm_split_js, ))); Some(manifest) diff --git a/leptos/src/lib.rs b/leptos/src/lib.rs index b6f6ae8c55..274a6cf34a 100644 --- a/leptos/src/lib.rs +++ b/leptos/src/lib.rs @@ -395,7 +395,8 @@ pub fn prefetch_lazy_fn_on_server(id: &'static str) { #[derive(Clone, Debug, Default)] pub struct WasmSplitManifest( pub reactive_graph::owner::ArcStoredValue<( - String, - std::collections::HashMap<String, Vec<String>>, + String, // the pkg root + std::collections::HashMap<String, Vec<String>>, // preloads + String, // the name of the __wasm_split.js file )>, ); From 036dced1561c8263a237dc37d7182c185bada579 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Sep 2025 17:01:08 -0400 Subject: [PATCH 38/91] chore(deps): bump the rust-dependencies group across 1 directory with 11 updates (#4319) Bumps the rust-dependencies group with 3 updates in the / directory: [anyhow](https://github.com/dtolnay/anyhow), [subsecond](https://github.com/dioxuslabs/dioxus) and [libloading](https://github.com/nagisa/rust_libloading). Updates `anyhow` from 1.0.99 to 1.0.100 - [Release notes](https://github.com/dtolnay/anyhow/releases) - [Commits](https://github.com/dtolnay/anyhow/compare/1.0.99...1.0.100) Updates `subsecond` from `eef4db6` to `2e7e069` - [Release notes](https://github.com/dioxuslabs/dioxus/releases) - [Commits](https://github.com/dioxuslabs/dioxus/compare/eef4db67b1164934eb29b7ac4d4bcd693bba25a5...2e7e0696a320a2a98a07e405603f59c8296b0b42) Updates `dioxus-cli-config` from `eef4db6` to `2e7e069` - [Release notes](https://github.com/dioxuslabs/dioxus/releases) - [Commits](https://github.com/dioxuslabs/dioxus/compare/eef4db67b1164934eb29b7ac4d4bcd693bba25a5...2e7e0696a320a2a98a07e405603f59c8296b0b42) Updates `dioxus-devtools` from `eef4db6` to `2e7e069` - [Release notes](https://github.com/dioxuslabs/dioxus/releases) - [Commits](https://github.com/dioxuslabs/dioxus/compare/eef4db67b1164934eb29b7ac4d4bcd693bba25a5...2e7e0696a320a2a98a07e405603f59c8296b0b42) Updates `dioxus-core` from `eef4db6` to `2e7e069` - [Release notes](https://github.com/DioxusLabs/dioxus/releases) - [Commits](https://github.com/DioxusLabs/dioxus/commits) Updates `dioxus-core-types` from `eef4db6` to `2e7e069` - [Release notes](https://github.com/DioxusLabs/dioxus/releases) - [Commits](https://github.com/DioxusLabs/dioxus/commits) Updates `dioxus-devtools-types` from `eef4db6` to `2e7e069` - [Release notes](https://github.com/DioxusLabs/dioxus/releases) - [Commits](https://github.com/DioxusLabs/dioxus/commits) Updates `dioxus-signals` from `eef4db6` to `2e7e069` - [Release notes](https://github.com/DioxusLabs/dioxus/releases) - [Commits](https://github.com/DioxusLabs/dioxus/commits) Updates `generational-box` from `eef4db6` to `2e7e069` - [Release notes](https://github.com/DioxusLabs/dioxus/releases) - [Commits](https://github.com/DioxusLabs/dioxus/commits) Updates `libloading` from 0.8.8 to 0.8.9 - [Commits](https://github.com/nagisa/rust_libloading/compare/0.8.8...0.8.9) Updates `subsecond-types` from `eef4db6` to `2e7e069` - [Release notes](https://github.com/DioxusLabs/dioxus/releases) - [Commits](https://github.com/DioxusLabs/dioxus/commits) --- updated-dependencies: - dependency-name: anyhow dependency-version: 1.0.100 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: subsecond dependency-version: 2e7e0696a320a2a98a07e405603f59c8296b0b42 dependency-type: direct:production dependency-group: rust-dependencies - dependency-name: dioxus-cli-config dependency-version: 2e7e0696a320a2a98a07e405603f59c8296b0b42 dependency-type: direct:production dependency-group: rust-dependencies - dependency-name: dioxus-devtools dependency-version: 2e7e0696a320a2a98a07e405603f59c8296b0b42 dependency-type: direct:production dependency-group: rust-dependencies - dependency-name: dioxus-core dependency-version: 2e7e0696a320a2a98a07e405603f59c8296b0b42 dependency-type: indirect dependency-group: rust-dependencies - dependency-name: dioxus-core-types dependency-version: 2e7e0696a320a2a98a07e405603f59c8296b0b42 dependency-type: indirect dependency-group: rust-dependencies - dependency-name: dioxus-devtools-types dependency-version: 2e7e0696a320a2a98a07e405603f59c8296b0b42 dependency-type: indirect dependency-group: rust-dependencies - dependency-name: dioxus-signals dependency-version: 2e7e0696a320a2a98a07e405603f59c8296b0b42 dependency-type: indirect dependency-group: rust-dependencies - dependency-name: generational-box dependency-version: 2e7e0696a320a2a98a07e405603f59c8296b0b42 dependency-type: indirect dependency-group: rust-dependencies - dependency-name: libloading dependency-version: 0.8.9 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: subsecond-types dependency-version: 2e7e0696a320a2a98a07e405603f59c8296b0b42 dependency-type: indirect dependency-group: rust-dependencies ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 36 ++++++++++++++++++------------------ Cargo.toml | 2 +- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9d183b0520..edde1e6a64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -279,9 +279,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.99" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "async-executor" @@ -903,12 +903,12 @@ dependencies = [ [[package]] name = "dioxus-cli-config" version = "0.7.0-rc.0" -source = "git+https://github.com/dioxuslabs/dioxus#eef4db67b1164934eb29b7ac4d4bcd693bba25a5" +source = "git+https://github.com/dioxuslabs/dioxus#2e7e0696a320a2a98a07e405603f59c8296b0b42" [[package]] name = "dioxus-core" version = "0.7.0-rc.0" -source = "git+https://github.com/dioxuslabs/dioxus#eef4db67b1164934eb29b7ac4d4bcd693bba25a5" +source = "git+https://github.com/dioxuslabs/dioxus#2e7e0696a320a2a98a07e405603f59c8296b0b42" dependencies = [ "const_format", "dioxus-core-types", @@ -929,12 +929,12 @@ dependencies = [ [[package]] name = "dioxus-core-types" version = "0.7.0-rc.0" -source = "git+https://github.com/dioxuslabs/dioxus#eef4db67b1164934eb29b7ac4d4bcd693bba25a5" +source = "git+https://github.com/dioxuslabs/dioxus#2e7e0696a320a2a98a07e405603f59c8296b0b42" [[package]] name = "dioxus-devtools" version = "0.7.0-rc.0" -source = "git+https://github.com/dioxuslabs/dioxus#eef4db67b1164934eb29b7ac4d4bcd693bba25a5" +source = "git+https://github.com/dioxuslabs/dioxus#2e7e0696a320a2a98a07e405603f59c8296b0b42" dependencies = [ "dioxus-cli-config", "dioxus-core", @@ -952,7 +952,7 @@ dependencies = [ [[package]] name = "dioxus-devtools-types" version = "0.7.0-rc.0" -source = "git+https://github.com/dioxuslabs/dioxus#eef4db67b1164934eb29b7ac4d4bcd693bba25a5" +source = "git+https://github.com/dioxuslabs/dioxus#2e7e0696a320a2a98a07e405603f59c8296b0b42" dependencies = [ "dioxus-core", "serde", @@ -962,7 +962,7 @@ dependencies = [ [[package]] name = "dioxus-signals" version = "0.7.0-rc.0" -source = "git+https://github.com/dioxuslabs/dioxus#eef4db67b1164934eb29b7ac4d4bcd693bba25a5" +source = "git+https://github.com/dioxuslabs/dioxus#2e7e0696a320a2a98a07e405603f59c8296b0b42" dependencies = [ "dioxus-core", "futures-channel", @@ -1051,7 +1051,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] @@ -1239,7 +1239,7 @@ dependencies = [ [[package]] name = "generational-box" version = "0.7.0-rc.0" -source = "git+https://github.com/dioxuslabs/dioxus#eef4db67b1164934eb29b7ac4d4bcd693bba25a5" +source = "git+https://github.com/dioxuslabs/dioxus#2e7e0696a320a2a98a07e405603f59c8296b0b42" dependencies = [ "parking_lot", "tracing", @@ -2148,12 +2148,12 @@ checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "libloading" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-targets 0.53.3", + "windows-link 0.2.0", ] [[package]] @@ -3137,7 +3137,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] @@ -3638,7 +3638,7 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "subsecond" version = "0.7.0-rc.0" -source = "git+https://github.com/dioxuslabs/dioxus#eef4db67b1164934eb29b7ac4d4bcd693bba25a5" +source = "git+https://github.com/dioxuslabs/dioxus#2e7e0696a320a2a98a07e405603f59c8296b0b42" dependencies = [ "js-sys", "libc", @@ -3656,7 +3656,7 @@ dependencies = [ [[package]] name = "subsecond-types" version = "0.7.0-rc.0" -source = "git+https://github.com/dioxuslabs/dioxus#eef4db67b1164934eb29b7ac4d4bcd693bba25a5" +source = "git+https://github.com/dioxuslabs/dioxus#2e7e0696a320a2a98a07e405603f59c8296b0b42" dependencies = [ "serde", ] @@ -3804,7 +3804,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] @@ -4611,7 +4611,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 7553b1a498..709c103d11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -125,7 +125,7 @@ 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.99" } +anyhow = { default-features = false, version = "1.0.100" } walkdir = { default-features = false, version = "2.5.0" } actix-ws = { default-features = false, version = "0.3.0" } tower-http = { default-features = false, version = "0.6.4" } From 24222f844bd49304986ef50224d4486e4ec916fa Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Wed, 24 Sep 2025 17:01:25 -0400 Subject: [PATCH 39/91] fix: correctly import scoped slots (closes #4311) (#4318) --- leptos_macro/src/view/mod.rs | 27 ++++++++++++++++++++++++--- leptos_macro/src/view/slot_helper.rs | 6 +++--- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/leptos_macro/src/view/mod.rs b/leptos_macro/src/view/mod.rs index febc0b077b..26b23aa638 100644 --- a/leptos_macro/src/view/mod.rs +++ b/leptos_macro/src/view/mod.rs @@ -25,9 +25,8 @@ use std::{ use syn::{ punctuated::Pair::{End, Punctuated}, spanned::Spanned, - Expr, - Expr::Tuple, - ExprArray, ExprLit, ExprRange, Lit, LitStr, RangeLimits, Stmt, + Expr::{self, Tuple}, + ExprArray, ExprLit, ExprPath, ExprRange, Lit, LitStr, RangeLimits, Stmt, }; #[derive(Clone, Copy, PartialEq, Eq)] @@ -1871,6 +1870,28 @@ pub(crate) fn ident_from_tag_name(tag_name: &NodeName) -> Ident { } } +pub(crate) fn full_path_from_tag_name(tag_name: &NodeName) -> Option<ExprPath> { + match tag_name { + NodeName::Path(path) => Some(path.clone()), + NodeName::Block(_) => { + let span = tag_name.span(); + proc_macro_error2::emit_error!( + span, + "blocks not allowed in tag-name position" + ); + None + } + _ => { + let span = tag_name.span(); + proc_macro_error2::emit_error!( + span, + "punctuated names not allowed in slots" + ); + None + } + } +} + pub(crate) fn directive_call_from_attribute_node( attr: &KeyedAttribute, directive_name: &str, diff --git a/leptos_macro/src/view/slot_helper.rs b/leptos_macro/src/view/slot_helper.rs index 47afa5d2d7..98d423c5d3 100644 --- a/leptos_macro/src/view/slot_helper.rs +++ b/leptos_macro/src/view/slot_helper.rs @@ -1,6 +1,6 @@ use super::{ component_builder::maybe_optimised_component_children, - convert_to_snake_case, ident_from_tag_name, + convert_to_snake_case, full_path_from_tag_name, }; use crate::view::{fragment_to_tokens, utils::filter_prefixed_attrs, TagType}; use proc_macro2::{Ident, TokenStream, TokenTree}; @@ -24,7 +24,7 @@ pub(crate) fn slot_to_tokens( node.name().to_string() }); - let component_name = ident_from_tag_name(node.name()); + let component_path = full_path_from_tag_name(node.name()); let Some(parent_slots) = parent_slots else { proc_macro_error2::emit_error!( @@ -190,7 +190,7 @@ pub(crate) fn slot_to_tokens( let slot = quote_spanned! {node.span()=> { - let slot = #component_name::builder() + let slot = #component_path::builder() #(#props)* #(#slots)* #children From ba9c63b03c97cd05f97883254ee030a5bb4eb600 Mon Sep 17 00:00:00 2001 From: zakstucke <44890343+zakstucke@users.noreply.github.com> Date: Thu, 25 Sep 2025 00:04:26 +0300 Subject: [PATCH 40/91] feat: allow accessing a parent owner from a child (#4325) --- reactive_graph/src/owner.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/reactive_graph/src/owner.rs b/reactive_graph/src/owner.rs index b0a7e3101f..8ad9a7d669 100644 --- a/reactive_graph/src/owner.rs +++ b/reactive_graph/src/owner.rs @@ -209,6 +209,25 @@ impl Owner { this } + /// Returns the parent of this `Owner`, if any. + /// + /// None when: + /// - This is a root owner + /// - The parent has been dropped + pub fn parent(&self) -> Option<Owner> { + self.inner + .read() + .or_poisoned() + .parent + .as_ref() + .and_then(|p| p.upgrade()) + .map(|inner| Owner { + inner, + #[cfg(feature = "hydration")] + shared_context: self.shared_context.clone(), + }) + } + /// Creates a new `Owner` that is the child of the current `Owner`, if any. pub fn child(&self) -> Self { let parent = Some(Arc::downgrade(&self.inner)); From 21082b626398ae66c1e8caef4a4085fa14de4851 Mon Sep 17 00:00:00 2001 From: Adam Doyle <adam@doordesk.net> Date: Fri, 19 Sep 2025 16:00:05 -0400 Subject: [PATCH 41/91] chore: add missing `dirname` attribute to `input` element --- tachys/src/html/element/elements.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tachys/src/html/element/elements.rs b/tachys/src/html/element/elements.rs index 17328e0be3..27a2e5eea6 100644 --- a/tachys/src/html/element/elements.rs +++ b/tachys/src/html/element/elements.rs @@ -212,7 +212,7 @@ html_self_closing_elements! { /// The `<img>` HTML element embeds an image into the document. img HtmlImageElement [alt, attributionsrc, crossorigin, decoding, elementtiming, fetchpriority, height, ismap, loading, referrerpolicy, sizes, src, srcset, usemap, width] true, /// The `<input>` HTML element is used to create interactive controls for web-based forms in order to accept data from the user; a wide variety of types of input data and control widgets are available, depending on the device and user agent. The `<input>` element is one of the most powerful and complex in all of HTML due to the sheer number of combinations of input types and attributes. - input HtmlInputElement [accept, alt, autocomplete, capture, checked, disabled, form, formaction, formenctype, formmethod, formnovalidate, formtarget, height, list, max, maxlength, min, minlength, multiple, name, pattern, placeholder, popovertarget, popovertargetaction, readonly, required, size, src, step, r#type, value, width] true, + input HtmlInputElement [accept, alt, autocomplete, capture, checked, dirname, disabled, form, formaction, formenctype, formmethod, formnovalidate, formtarget, height, list, max, maxlength, min, minlength, multiple, name, pattern, placeholder, popovertarget, popovertargetaction, readonly, required, size, src, step, r#type, value, width] true, /// The `<link>` HTML element specifies relationships between the current document and an external resource. This element is most commonly used to link to CSS, but is also used to establish site icons (both "favicon" style icons and icons for the home screen and apps on mobile devices) among other things. link HtmlLinkElement [r#as, blocking, crossorigin, fetchpriority, href, hreflang, imagesizes, imagesrcset, integrity, media, rel, referrerpolicy, sizes, r#type] true, /// The `<meta>` HTML element represents Metadata that cannot be represented by other HTML meta-related elements, like base, link, script, style or title. From 7f7772dabda4a12f86b925898cbe8edbfece4158 Mon Sep 17 00:00:00 2001 From: Adam Doyle <adam@doordesk.net> Date: Sat, 20 Sep 2025 08:40:17 -0400 Subject: [PATCH 42/91] chore: add missing `exportparts` global attribute --- tachys/src/html/attribute/global.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tachys/src/html/attribute/global.rs b/tachys/src/html/attribute/global.rs index 62bfd3c965..ab2bb29ae4 100644 --- a/tachys/src/html/attribute/global.rs +++ b/tachys/src/html/attribute/global.rs @@ -205,6 +205,14 @@ where self.add_any_attr(enterkeyhint(value)) } + /// The `exportparts` attribute enables the sharing of parts of an element's shadow DOM with a containing document. + fn exportparts( + self, + value: V, + ) -> <Self as AddAnyAttr>::Output<Attr<Exportparts, V>> { + self.add_any_attr(exportparts(value)) + } + /// The `hidden` global attribute is a Boolean attribute indicating that the element is not yet, or is no longer, relevant. fn hidden(self, value: V) -> <Self as AddAnyAttr>::Output<Attr<Hidden, V>> { self.add_any_attr(hidden(value)) From 8bb203d2d36083b79bd827a401b0425fca19776c Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Wed, 24 Sep 2025 19:23:13 -0400 Subject: [PATCH 43/91] test: add regression test for #4251 --- .../e2e/features/issue_4251.feature | 13 ++++++++++++ .../e2e/tests/fixtures/world/action_steps.rs | 9 ++++++++ examples/regression/src/app.rs | 6 ++++-- examples/regression/src/issue_4324.rs | 21 +++++++++++++++++++ examples/regression/src/lib.rs | 1 + 5 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 examples/regression/e2e/features/issue_4251.feature create mode 100644 examples/regression/src/issue_4324.rs diff --git a/examples/regression/e2e/features/issue_4251.feature b/examples/regression/e2e/features/issue_4251.feature new file mode 100644 index 0000000000..31a55935ef --- /dev/null +++ b/examples/regression/e2e/features/issue_4251.feature @@ -0,0 +1,13 @@ +@check_issue_4251 +Feature: Check that issue 4251 does not reappear + + Scenario: Clicking a link to the same page you’re currently on should not add the page to the history stack. + Given I see the app + And I can access regression test 4324 + When I select the link This page + And I select the link This page + And I select the link This page + Then I see the result is the string Issue4324 + When I press the back button + And I select the link 4324 + Then I see the result is the string Issue4324 \ No newline at end of file diff --git a/examples/regression/e2e/tests/fixtures/world/action_steps.rs b/examples/regression/e2e/tests/fixtures/world/action_steps.rs index 0bc50bf8e5..c0f160dc36 100644 --- a/examples/regression/e2e/tests/fixtures/world/action_steps.rs +++ b/examples/regression/e2e/tests/fixtures/world/action_steps.rs @@ -45,3 +45,12 @@ async fn i_refresh_the_browser(world: &mut AppWorld) -> Result<()> { Ok(()) } + +#[given(regex = "^I press the back button$")] +#[when(regex = "^I press the back button$")] +async fn i_go_back(world: &mut AppWorld) -> Result<()> { + let client = &world.client; + client.back().await?; + + Ok(()) +} diff --git a/examples/regression/src/app.rs b/examples/regression/src/app.rs index 134a92d2cc..c2a3f85ba1 100644 --- a/examples/regression/src/app.rs +++ b/examples/regression/src/app.rs @@ -1,7 +1,7 @@ use crate::{ issue_4005::Routes4005, issue_4088::Routes4088, issue_4217::Routes4217, - issue_4285::Routes4285, issue_4296::Routes4296, pr_4015::Routes4015, - pr_4091::Routes4091, + issue_4285::Routes4285, issue_4296::Routes4296, issue_4324::Routes4324, + pr_4015::Routes4015, pr_4091::Routes4091, }; use leptos::prelude::*; use leptos_meta::{MetaTags, *}; @@ -47,6 +47,7 @@ pub fn App() -> impl IntoView { <Routes4005/> <Routes4285/> <Routes4296/> + <Routes4324/> </Routes> </main> </Router> @@ -73,6 +74,7 @@ fn HomePage() -> impl IntoView { <li><a href="/4005/">"4005"</a></li> <li><a href="/4285/">"4285"</a></li> <li><a href="/4296/">"4296"</a></li> + <li><a href="/4324/">"4324"</a></li> </ul> </nav> } diff --git a/examples/regression/src/issue_4324.rs b/examples/regression/src/issue_4324.rs new file mode 100644 index 0000000000..83e0883f59 --- /dev/null +++ b/examples/regression/src/issue_4324.rs @@ -0,0 +1,21 @@ +use leptos::prelude::*; +#[allow(unused_imports)] +use leptos_router::{ + components::Route, path, Lazy, MatchNestedRoutes, NavigateOptions, +}; + +#[component] +pub fn Routes4324() -> impl MatchNestedRoutes + Clone { + view! { + <Route path=path!("4324") view=Issue4324/> + } + .into_inner() +} + +#[component] +pub fn Issue4324() -> impl IntoView { + view! { + <a href="/4324/">"This page"</a> + <p id="result">"Issue4324"</p> + } +} diff --git a/examples/regression/src/lib.rs b/examples/regression/src/lib.rs index a326d4e4df..acb5aecee3 100644 --- a/examples/regression/src/lib.rs +++ b/examples/regression/src/lib.rs @@ -4,6 +4,7 @@ mod issue_4088; mod issue_4217; mod issue_4285; mod issue_4296; +mod issue_4324; mod pr_4015; mod pr_4091; From ca641e742fdc1747ba210c8b4ecd5acedace4fb1 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Wed, 24 Sep 2025 19:23:20 -0400 Subject: [PATCH 44/91] test: add regression test for #4324 --- examples/regression/e2e/features/issue_4324.feature | 11 +++++++++++ examples/regression/e2e/tests/fixtures/check.rs | 10 ++++++++++ .../e2e/tests/fixtures/world/check_steps.rs | 7 +++++++ 3 files changed, 28 insertions(+) create mode 100644 examples/regression/e2e/features/issue_4324.feature diff --git a/examples/regression/e2e/features/issue_4324.feature b/examples/regression/e2e/features/issue_4324.feature new file mode 100644 index 0000000000..e08da31c9a --- /dev/null +++ b/examples/regression/e2e/features/issue_4324.feature @@ -0,0 +1,11 @@ +@check_issue_4324 +Feature: Check that issue 4324 does not reappear + + Scenario: Navigating to the same page after clicking "Back" should set the URL correctly + Given I see the app + And I can access regression test 4324 + Then I see the path is /4324/ + When I press the back button + Then I see the path is / + When I select the link 4324 + Then I see the path is /4324/ \ No newline at end of file diff --git a/examples/regression/e2e/tests/fixtures/check.rs b/examples/regression/e2e/tests/fixtures/check.rs index aceb249cbb..e021171f4f 100644 --- a/examples/regression/e2e/tests/fixtures/check.rs +++ b/examples/regression/e2e/tests/fixtures/check.rs @@ -43,3 +43,13 @@ pub async fn element_value_is( assert_eq!(value.as_deref(), Some(expected)); Ok(()) } + +pub async fn path_is(client: &Client, expected_path: &str) -> Result<()> { + let url = client + .current_url() + .await + .expect("could not access current URL"); + let path = url.path(); + assert_eq!(expected_path, path); + Ok(()) +} diff --git a/examples/regression/e2e/tests/fixtures/world/check_steps.rs b/examples/regression/e2e/tests/fixtures/world/check_steps.rs index 4bf1a71397..51255fedc8 100644 --- a/examples/regression/e2e/tests/fixtures/world/check_steps.rs +++ b/examples/regression/e2e/tests/fixtures/world/check_steps.rs @@ -43,3 +43,10 @@ async fn i_see_the_value( check::element_value_is(client, &id, &value).await?; Ok(()) } + +#[then(regex = r"^I see the path is (.*)$")] +async fn i_see_the_path(world: &mut AppWorld, path: String) -> Result<()> { + let client = &world.client; + check::path_is(client, &path).await?; + Ok(()) +} From 7bf4d341fdb6d82b93cfb2d5a6727769e7508e7a Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Wed, 24 Sep 2025 19:39:31 -0400 Subject: [PATCH 45/91] fix: correctly update path stack when navigating backwards (closes #4324) --- router/src/location/history.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/router/src/location/history.rs b/router/src/location/history.rs index 8bbfdb09d6..94481cf741 100644 --- a/router/src/location/history.rs +++ b/router/src/location/history.rs @@ -185,11 +185,15 @@ impl LocationProvider for BrowserUrl { let is_back = self.is_back.clone(); move || match Self::current() { Ok(new_url) => { - let stack = path_stack.read_value(); + let mut stack = path_stack.write_value(); let is_navigating_back = stack.len() == 1 || (stack.len() >= 2 && stack.get(stack.len() - 2) == Some(&new_url)); + if is_navigating_back { + stack.pop(); + } + is_back.set(is_navigating_back); url.set(new_url); From ab2f973e30cccaab20f6649705e152491ee8da21 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Fri, 26 Sep 2025 07:45:41 -0400 Subject: [PATCH 46/91] fix: correctly poll all out-of-order streaming chunks (closes #4326) (#4333) * fix: correctly poll all out-of-order streaming chunks (closes #4326) * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- tachys/src/ssr/mod.rs | 146 ++++++++++++++++++++++-------------------- 1 file changed, 77 insertions(+), 69 deletions(-) diff --git a/tachys/src/ssr/mod.rs b/tachys/src/ssr/mod.rs index 61cfdb8b42..8a940c8ac1 100644 --- a/tachys/src/ssr/mod.rs +++ b/tachys/src/ssr/mod.rs @@ -378,83 +378,91 @@ impl Stream for StreamBuilder { let next_chunk = this.chunks.pop_front(); match next_chunk { None => { - // now, handle out-of-order chunks - if let Some(mut pending) = this.pending_ooo.pop_front() { - match pending.as_mut().poll(cx) { - Poll::Ready(OooChunk { - id, - chunks, - replace, - nonce, - }) => { - let opening = format!("<!--s-{id}o-->"); - let placeholder_at = - this.sync_buf.find(&opening); - if let Some(start) = placeholder_at { - let closing = format!("<!--s-{id}c-->"); - let end = - this.sync_buf.find(&closing).unwrap(); - let chunks_iter = chunks.into_iter().rev(); - - // TODO can probably make this more efficient - let (before, replaced) = - this.sync_buf.split_at(start); - let (_, after) = replaced - .split_at(end - start + closing.len()); - let mut buf = String::new(); - buf.push_str(before); - - let mut held_chunks = VecDeque::new(); - for chunk in chunks_iter { - if let StreamChunk::Sync(ready) = chunk - { - buf.push_str(&ready); - } else { - held_chunks.push_front(chunk); + if this.pending_ooo.is_empty() { + if this.sync_buf.is_empty() { + Poll::Ready(None) + } else { + Poll::Ready(Some(mem::take(&mut this.sync_buf))) + } + } else { + // check if *any* pending out-of-order chunk is ready + for mut chunk in mem::take(&mut this.pending_ooo) { + match chunk.as_mut().poll(cx) { + Poll::Ready(OooChunk { + id, + chunks, + replace, + nonce, + }) => { + let opening = format!("<!--s-{id}o-->"); + let placeholder_at = + this.sync_buf.find(&opening); + if let Some(start) = placeholder_at { + let closing = format!("<!--s-{id}c-->"); + let end = this + .sync_buf + .find(&closing) + .unwrap(); + let chunks_iter = + chunks.into_iter().rev(); + + // TODO can probably make this more efficient + let (before, replaced) = + this.sync_buf.split_at(start); + let (_, after) = replaced.split_at( + end - start + closing.len(), + ); + let mut buf = String::new(); + buf.push_str(before); + + let mut held_chunks = VecDeque::new(); + for chunk in chunks_iter { + if let StreamChunk::Sync(ready) = + chunk + { + buf.push_str(&ready); + } else { + held_chunks.push_front(chunk); + } } - } - buf.push_str(after); - this.sync_buf = buf; - for chunk in held_chunks { - this.chunks.push_front(chunk); - } - } else { - OooChunk::push_start( - &id, - &mut this.sync_buf, - ); - for chunk in chunks.into_iter().rev() { - if let StreamChunk::Sync(ready) = chunk - { - this.sync_buf.push_str(&ready); - } else { + buf.push_str(after); + this.sync_buf = buf; + for chunk in held_chunks { this.chunks.push_front(chunk); } + } else { + OooChunk::push_start( + &id, + &mut this.sync_buf, + ); + for chunk in chunks.into_iter().rev() { + if let StreamChunk::Sync(ready) = + chunk + { + this.sync_buf.push_str(&ready); + } else { + this.chunks.push_front(chunk); + } + } + OooChunk::push_end_with_nonce( + replace, + &id, + &mut this.sync_buf, + nonce.as_deref(), + ); } - OooChunk::push_end_with_nonce( - replace, - &id, - &mut this.sync_buf, - nonce.as_deref(), - ); } - self.poll_next(cx) - } - Poll::Pending => { - this.pending_ooo.push_back(pending); - if this.sync_buf.is_empty() { - Poll::Pending - } else { - Poll::Ready(Some(mem::take( - &mut this.sync_buf, - ))) + Poll::Pending => { + this.pending_ooo.push_back(chunk); } } } - } else if this.sync_buf.is_empty() { - Poll::Ready(None) - } else { - Poll::Ready(Some(mem::take(&mut this.sync_buf))) + + if this.sync_buf.is_empty() { + Poll::Pending + } else { + Poll::Ready(Some(mem::take(&mut this.sync_buf))) + } } } Some(StreamChunk::Sync(value)) => { From 194db0c93007b1b3af23e356a6f6c1c44dce7039 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Mon, 29 Sep 2025 15:21:22 -0400 Subject: [PATCH 47/91] chore: publish patch releases --- Cargo.lock | 41 +++++++++++++++++++++-------------- Cargo.toml | 20 ++++++++--------- integrations/utils/Cargo.toml | 2 +- leptos/Cargo.toml | 2 +- leptos_dom/Cargo.toml | 2 +- leptos_macro/Cargo.toml | 2 +- reactive_graph/Cargo.toml | 2 +- router/Cargo.toml | 2 +- tachys/Cargo.toml | 2 +- 9 files changed, 42 insertions(+), 33 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index edde1e6a64..2af34ca4a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -903,12 +903,14 @@ dependencies = [ [[package]] name = "dioxus-cli-config" version = "0.7.0-rc.0" -source = "git+https://github.com/dioxuslabs/dioxus#2e7e0696a320a2a98a07e405603f59c8296b0b42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8cec511d8a05ed60071bb0088f07ec40325faf27a608fa19d65befdd842b57f" [[package]] name = "dioxus-core" version = "0.7.0-rc.0" -source = "git+https://github.com/dioxuslabs/dioxus#2e7e0696a320a2a98a07e405603f59c8296b0b42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07b55eccaa5c4f35f1755ea18a5716fe8ecba60ff1f25c52be6ceda2e6a52eb6" dependencies = [ "const_format", "dioxus-core-types", @@ -929,12 +931,14 @@ dependencies = [ [[package]] name = "dioxus-core-types" version = "0.7.0-rc.0" -source = "git+https://github.com/dioxuslabs/dioxus#2e7e0696a320a2a98a07e405603f59c8296b0b42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0ef2a94b4ceb8f7a39f56a539d07e82b0358a49a0b95028ad48635975df29d7" [[package]] name = "dioxus-devtools" version = "0.7.0-rc.0" -source = "git+https://github.com/dioxuslabs/dioxus#2e7e0696a320a2a98a07e405603f59c8296b0b42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60af4e129968ab1713471ed0b29c3eefa4e8e09252429487d7a581e93c7ecfe5" dependencies = [ "dioxus-cli-config", "dioxus-core", @@ -952,7 +956,8 @@ dependencies = [ [[package]] name = "dioxus-devtools-types" version = "0.7.0-rc.0" -source = "git+https://github.com/dioxuslabs/dioxus#2e7e0696a320a2a98a07e405603f59c8296b0b42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64274704b6a8d018112473cdce0b3db1dcccfa79bde445223592fe4e396ff5ef" dependencies = [ "dioxus-core", "serde", @@ -962,7 +967,8 @@ dependencies = [ [[package]] name = "dioxus-signals" version = "0.7.0-rc.0" -source = "git+https://github.com/dioxuslabs/dioxus#2e7e0696a320a2a98a07e405603f59c8296b0b42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1d0e70a8da969c0404f5ef8cf6f47042bea55608b582079f3ea3d9fff46125c" dependencies = [ "dioxus-core", "futures-channel", @@ -1239,7 +1245,8 @@ dependencies = [ [[package]] name = "generational-box" version = "0.7.0-rc.0" -source = "git+https://github.com/dioxuslabs/dioxus#2e7e0696a320a2a98a07e405603f59c8296b0b42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb058e0358ff765e719ab3e61629c5090fedb6a6ccf66479de21440a33d7f084" dependencies = [ "parking_lot", "tracing", @@ -1873,7 +1880,7 @@ checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" [[package]] name = "leptos" -version = "0.8.9" +version = "0.8.10" dependencies = [ "any_spawner", "base64", @@ -1994,7 +2001,7 @@ dependencies = [ [[package]] name = "leptos_dom" -version = "0.8.6" +version = "0.8.7" dependencies = [ "js-sys", "leptos", @@ -2027,7 +2034,7 @@ dependencies = [ [[package]] name = "leptos_integration_utils" -version = "0.8.5" +version = "0.8.6" dependencies = [ "futures", "hydration_context", @@ -2040,7 +2047,7 @@ dependencies = [ [[package]] name = "leptos_macro" -version = "0.8.8" +version = "0.8.9" dependencies = [ "attribute-derive", "cfg-if", @@ -2084,7 +2091,7 @@ dependencies = [ [[package]] name = "leptos_router" -version = "0.8.7" +version = "0.8.8" dependencies = [ "any_spawner", "either_of", @@ -2868,7 +2875,7 @@ dependencies = [ [[package]] name = "reactive_graph" -version = "0.2.7" +version = "0.2.8" dependencies = [ "any_spawner", "async-lock", @@ -3638,7 +3645,8 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "subsecond" version = "0.7.0-rc.0" -source = "git+https://github.com/dioxuslabs/dioxus#2e7e0696a320a2a98a07e405603f59c8296b0b42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b14ed4d86ab065ffbfdb994fd3e44daf5244b02cb643bd52949d74b703f36605" dependencies = [ "js-sys", "libc", @@ -3656,7 +3664,8 @@ dependencies = [ [[package]] name = "subsecond-types" version = "0.7.0-rc.0" -source = "git+https://github.com/dioxuslabs/dioxus#2e7e0696a320a2a98a07e405603f59c8296b0b42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "275920a8a5634e47e12253971db85946798795bbe4d9dfc1debf23533d823983" dependencies = [ "serde", ] @@ -3735,7 +3744,7 @@ dependencies = [ [[package]] name = "tachys" -version = "0.2.8" +version = "0.2.9" dependencies = [ "any_spawner", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 709c103d11..a86392695b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,26 +50,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.9" } +leptos = { path = "./leptos", version = "0.8.10" } leptos_config = { path = "./leptos_config", version = "0.8.7" } -leptos_dom = { path = "./leptos_dom", version = "0.8.6" } +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.5" } -leptos_macro = { path = "./leptos_macro", version = "0.8.8" } -leptos_router = { path = "./router", version = "0.8.7" } +leptos_integration_utils = { path = "./integrations/utils", version = "0.8.6" } +leptos_macro = { path = "./leptos_macro", version = "0.8.9" } +leptos_router = { path = "./router", version = "0.8.8" } leptos_router_macro = { path = "./router_macro", version = "0.8.5" } leptos_server = { path = "./leptos_server", version = "0.8.5" } 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.7" } +reactive_graph = { path = "./reactive_graph", version = "0.2.8" } reactive_stores = { path = "./reactive_stores", version = "0.2.5" } reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.2.6" } server_fn = { path = "./server_fn", version = "0.8.7" } server_fn_macro = { path = "./server_fn_macro", version = "0.8.7" } server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.5" } -tachys = { path = "./tachys", version = "0.2.8" } +tachys = { path = "./tachys", version = "0.2.9" } wasm_split_helpers = { path = "./wasm_split", version = "0.1.2" } wasm_split_macros = { path = "./wasm_split_macros", version = "0.1.3" } @@ -170,9 +170,9 @@ async-lock = { default-features = false, version = "3.4.1" } base16 = { default-features = false, version = "0.2.1" } digest = { default-features = false, version = "0.10.7" } sha2 = { default-features = false, version = "0.10.8" } -subsecond = { default-features = false, git = "https://github.com/dioxuslabs/dioxus" } -dioxus-cli-config = { default-features = false, git = "https://github.com/dioxuslabs/dioxus" } -dioxus-devtools = { default-features = false, git = "https://github.com/dioxuslabs/dioxus" } +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" } [profile.release] codegen-units = 1 diff --git a/integrations/utils/Cargo.toml b/integrations/utils/Cargo.toml index 812b395e18..e497f58520 100644 --- a/integrations/utils/Cargo.toml +++ b/integrations/utils/Cargo.toml @@ -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.5" +version = "0.8.6" rust-version.workspace = true edition.workspace = true diff --git a/leptos/Cargo.toml b/leptos/Cargo.toml index 2550394020..c630e9c473 100644 --- a/leptos/Cargo.toml +++ b/leptos/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "leptos" -version = "0.8.9" +version = "0.8.10" authors = ["Greg Johnston"] license = "MIT" repository = "https://github.com/leptos-rs/leptos" diff --git a/leptos_dom/Cargo.toml b/leptos_dom/Cargo.toml index 1fd3407344..78bc5f7d41 100644 --- a/leptos_dom/Cargo.toml +++ b/leptos_dom/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "leptos_dom" -version = "0.8.6" +version = "0.8.7" authors = ["Greg Johnston"] license = "MIT" repository = "https://github.com/leptos-rs/leptos" diff --git a/leptos_macro/Cargo.toml b/leptos_macro/Cargo.toml index 141b49ea6c..35449becd1 100644 --- a/leptos_macro/Cargo.toml +++ b/leptos_macro/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "leptos_macro" -version = "0.8.8" +version = "0.8.9" authors = ["Greg Johnston"] license = "MIT" repository = "https://github.com/leptos-rs/leptos" diff --git a/reactive_graph/Cargo.toml b/reactive_graph/Cargo.toml index e32e19cd51..a073966e5c 100644 --- a/reactive_graph/Cargo.toml +++ b/reactive_graph/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "reactive_graph" -version = "0.2.7" +version = "0.2.8" authors = ["Greg Johnston"] license = "MIT" readme = "../README.md" diff --git a/router/Cargo.toml b/router/Cargo.toml index 22a20044fa..877a5f1ad9 100644 --- a/router/Cargo.toml +++ b/router/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "leptos_router" -version = "0.8.7" +version = "0.8.8" authors = ["Greg Johnston", "Ben Wishovich"] license = "MIT" readme = "../README.md" diff --git a/tachys/Cargo.toml b/tachys/Cargo.toml index e4e8003c76..bcab009f3c 100644 --- a/tachys/Cargo.toml +++ b/tachys/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tachys" -version = "0.2.8" +version = "0.2.9" authors = ["Greg Johnston"] license = "MIT" readme = "../README.md" From ec428ed806251a088577f7944087bc13b05bc8e2 Mon Sep 17 00:00:00 2001 From: Brett Etter <bbetter9101@gmail.com> Date: Sat, 4 Oct 2025 08:07:04 -0500 Subject: [PATCH 48/91] fixed: removed excess bound on `MaybeProp` `ReadUntracked` implementation. (#4360) --- reactive_graph/src/wrappers.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/reactive_graph/src/wrappers.rs b/reactive_graph/src/wrappers.rs index b91a27b285..4762e02b39 100644 --- a/reactive_graph/src/wrappers.rs +++ b/reactive_graph/src/wrappers.rs @@ -1542,7 +1542,6 @@ pub mod read { impl<T, S> ReadUntracked for MaybeProp<T, S> where - T: Clone, S: Storage<Option<T>> + Storage<SignalTypes<Option<T>, S>>, { type Value = ReadGuard<Option<T>, SignalReadGuard<Option<T>, S>>; From 74f4a85fb41832cad6b67da1c00ed3fbfcf4fb60 Mon Sep 17 00:00:00 2001 From: Adam Doyle <24621027+adoyle0@users.noreply.github.com> Date: Fri, 10 Oct 2025 09:56:23 -0400 Subject: [PATCH 49/91] chore: add `scrollend` event to view macro (#4379) --- leptos_macro/src/view/mod.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/leptos_macro/src/view/mod.rs b/leptos_macro/src/view/mod.rs index 26b23aa638..1551035cdf 100644 --- a/leptos_macro/src/view/mod.rs +++ b/leptos_macro/src/view/mod.rs @@ -1678,7 +1678,7 @@ fn attribute_value( } // Keep list alphabetized for binary search -const TYPED_EVENTS: [&str; 126] = [ +const TYPED_EVENTS: [&str; 127] = [ "DOMContentLoaded", "abort", "afterprint", @@ -1774,6 +1774,7 @@ const TYPED_EVENTS: [&str; 126] = [ "reset", "resize", "scroll", + "scrollend", "securitypolicyviolation", "seeked", "seeking", From 777d669b4fbd6b71916fc2e3640f487d3ae29623 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Oct 2025 11:25:54 -0400 Subject: [PATCH 50/91] chore(deps): bump the rust-dependencies group with 43 updates (#4368) Bumps the rust-dependencies group with 43 updates: | Package | From | To | | --- | --- | --- | | [thiserror](https://github.com/dtolnay/thiserror) | `2.0.16` | `2.0.17` | | [const_format](https://github.com/rodrimati1992/const_format_crates) | `0.2.34` | `0.2.35` | | [parking_lot](https://github.com/Amanieu/parking_lot) | `0.12.4` | `0.12.5` | | [axum](https://github.com/tokio-rs/axum) | `0.8.4` | `0.8.6` | | [quote](https://github.com/dtolnay/quote) | `1.0.40` | `1.0.41` | | [tokio-tungstenite](https://github.com/snapview/tokio-tungstenite) | `0.27.0` | `0.28.0` | | [camino](https://github.com/camino-rs/camino) | `1.2.0` | `1.2.1` | | [rkyv](https://github.com/rkyv/rkyv) | `0.8.11` | `0.8.12` | | [regex](https://github.com/rust-lang/regex) | `1.11.2` | `1.11.3` | | [tempfile](https://github.com/Stebalien/tempfile) | `3.22.0` | `3.23.0` | | [attribute-derive](https://github.com/ModProg/attribute-derive) | `0.10.3` | `0.10.5` | | [actix-http](https://github.com/actix/actix-web) | `3.11.1` | `3.11.2` | | [attribute-derive-macro](https://github.com/ModProg/attribute-derive) | `0.10.3` | `0.10.5` | | [axum-core](https://github.com/tokio-rs/axum) | `0.5.2` | `0.5.5` | | [backtrace](https://github.com/rust-lang/backtrace-rs) | `0.3.75` | `0.3.76` | | [bytecheck](https://github.com/rkyv/bytecheck) | `0.8.1` | `0.8.2` | | [bytecheck_derive](https://github.com/rkyv/bytecheck) | `0.8.1` | `0.8.2` | | [cc](https://github.com/rust-lang/cc-rs) | `1.2.38` | `1.2.40` | | [collection_literals](https://github.com/staedoix/collection_literals) | `1.0.2` | `1.0.3` | | [deranged](https://github.com/jhpratt/deranged) | `0.5.3` | `0.5.4` | | [find-msvc-tools](https://github.com/rust-lang/cc-rs) | `0.1.2` | `0.1.3` | | [flate2](https://github.com/rust-lang/flate2-rs) | `1.1.2` | `1.1.4` | | [gimli](https://github.com/gimli-rs/gimli) | `0.31.1` | `0.32.3` | | [libc](https://github.com/rust-lang/libc) | `0.2.175` | `0.2.176` | | [lock_api](https://github.com/Amanieu/parking_lot) | `0.4.13` | `0.4.14` | | [memchr](https://github.com/BurntSushi/memchr) | `2.7.5` | `2.7.6` | | [miniserde](https://github.com/dtolnay/miniserde) | `0.1.42` | `0.1.43` | | [munge](https://github.com/djkoloski/munge) | `0.4.6` | `0.4.7` | | [munge_macro](https://github.com/djkoloski/munge) | `0.4.6` | `0.4.7` | | [object](https://github.com/gimli-rs/object) | `0.36.7` | `0.37.3` | | [parking_lot_core](https://github.com/Amanieu/parking_lot) | `0.9.11` | `0.9.12` | | [ptr_meta](https://github.com/rkyv/ptr_meta) | `0.3.0` | `0.3.1` | | [ptr_meta_derive](https://github.com/rkyv/ptr_meta) | `0.3.0` | `0.3.1` | | [rancor](https://github.com/rkyv/rancor) | `0.1.0` | `0.1.1` | | redox_syscall | `0.5.17` | `0.5.18` | | [regex-automata](https://github.com/rust-lang/regex) | `0.4.10` | `0.4.11` | | [rend](https://github.com/djkoloski/rend) | `0.5.2` | `0.5.3` | | [rkyv_derive](https://github.com/rkyv/rkyv) | `0.8.11` | `0.8.12` | | [rustls-webpki](https://github.com/rustls/webpki) | `0.103.6` | `0.103.7` | | [tokio-rustls](https://github.com/rustls/tokio-rustls) | `0.26.3` | `0.26.4` | | [tungstenite](https://github.com/snapview/tungstenite-rs) | `0.26.2` | `0.27.0` | | [typenum](https://github.com/paholg/typenum) | `1.18.0` | `1.19.0` | | [zeroize](https://github.com/RustCrypto/utils) | `1.8.1` | `1.8.2` | Updates `thiserror` from 2.0.16 to 2.0.17 - [Release notes](https://github.com/dtolnay/thiserror/releases) - [Commits](https://github.com/dtolnay/thiserror/compare/2.0.16...2.0.17) Updates `const_format` from 0.2.34 to 0.2.35 - [Release notes](https://github.com/rodrimati1992/const_format_crates/releases) - [Changelog](https://github.com/rodrimati1992/const_format_crates/blob/master/Changelog.md) - [Commits](https://github.com/rodrimati1992/const_format_crates/commits) Updates `parking_lot` from 0.12.4 to 0.12.5 - [Release notes](https://github.com/Amanieu/parking_lot/releases) - [Changelog](https://github.com/Amanieu/parking_lot/blob/master/CHANGELOG.md) - [Commits](https://github.com/Amanieu/parking_lot/compare/parking_lot-v0.12.4...parking_lot-v0.12.5) Updates `axum` from 0.8.4 to 0.8.6 - [Release notes](https://github.com/tokio-rs/axum/releases) - [Changelog](https://github.com/tokio-rs/axum/blob/main/CHANGELOG.md) - [Commits](https://github.com/tokio-rs/axum/compare/axum-v0.8.4...axum-v0.8.6) Updates `quote` from 1.0.40 to 1.0.41 - [Release notes](https://github.com/dtolnay/quote/releases) - [Commits](https://github.com/dtolnay/quote/compare/1.0.40...1.0.41) Updates `tokio-tungstenite` from 0.27.0 to 0.28.0 - [Changelog](https://github.com/snapview/tokio-tungstenite/blob/master/CHANGELOG.md) - [Commits](https://github.com/snapview/tokio-tungstenite/compare/v0.27.0...v0.28.0) Updates `camino` from 1.2.0 to 1.2.1 - [Release notes](https://github.com/camino-rs/camino/releases) - [Changelog](https://github.com/camino-rs/camino/blob/main/CHANGELOG.md) - [Commits](https://github.com/camino-rs/camino/compare/camino-1.2.0...camino-1.2.1) Updates `rkyv` from 0.8.11 to 0.8.12 - [Release notes](https://github.com/rkyv/rkyv/releases) - [Commits](https://github.com/rkyv/rkyv/commits) Updates `regex` from 1.11.2 to 1.11.3 - [Release notes](https://github.com/rust-lang/regex/releases) - [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-lang/regex/compare/1.11.2...1.11.3) Updates `tempfile` from 3.22.0 to 3.23.0 - [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md) - [Commits](https://github.com/Stebalien/tempfile/compare/v3.22.0...v3.23.0) Updates `attribute-derive` from 0.10.3 to 0.10.5 - [Release notes](https://github.com/ModProg/attribute-derive/releases) - [Changelog](https://github.com/ModProg/attribute-derive/blob/main/CHANGELOG.md) - [Commits](https://github.com/ModProg/attribute-derive/compare/v0.10.3...v0.10.5) Updates `actix-http` from 3.11.1 to 3.11.2 - [Release notes](https://github.com/actix/actix-web/releases) - [Changelog](https://github.com/actix/actix-web/blob/master/CHANGES.md) - [Commits](https://github.com/actix/actix-web/compare/http-v3.11.1...http-v3.11.2) Updates `attribute-derive-macro` from 0.10.3 to 0.10.5 - [Release notes](https://github.com/ModProg/attribute-derive/releases) - [Changelog](https://github.com/ModProg/attribute-derive/blob/main/CHANGELOG.md) - [Commits](https://github.com/ModProg/attribute-derive/compare/v0.10.3...v0.10.5) Updates `axum-core` from 0.5.2 to 0.5.5 - [Release notes](https://github.com/tokio-rs/axum/releases) - [Changelog](https://github.com/tokio-rs/axum/blob/main/CHANGELOG.md) - [Commits](https://github.com/tokio-rs/axum/compare/axum-core-v0.5.2...axum-core-v0.5.5) Updates `backtrace` from 0.3.75 to 0.3.76 - [Release notes](https://github.com/rust-lang/backtrace-rs/releases) - [Changelog](https://github.com/rust-lang/backtrace-rs/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-lang/backtrace-rs/compare/0.3.75...backtrace-v0.3.76) Updates `bytecheck` from 0.8.1 to 0.8.2 - [Release notes](https://github.com/rkyv/bytecheck/releases) - [Commits](https://github.com/rkyv/bytecheck/commits) Updates `bytecheck_derive` from 0.8.1 to 0.8.2 - [Release notes](https://github.com/rkyv/bytecheck/releases) - [Commits](https://github.com/rkyv/bytecheck/commits) Updates `cc` from 1.2.38 to 1.2.40 - [Release notes](https://github.com/rust-lang/cc-rs/releases) - [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md) - [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.38...cc-v1.2.40) Updates `collection_literals` from 1.0.2 to 1.0.3 - [Commits](https://github.com/staedoix/collection_literals/commits) Updates `deranged` from 0.5.3 to 0.5.4 - [Commits](https://github.com/jhpratt/deranged/commits) Updates `find-msvc-tools` from 0.1.2 to 0.1.3 - [Release notes](https://github.com/rust-lang/cc-rs/releases) - [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md) - [Commits](https://github.com/rust-lang/cc-rs/compare/find-msvc-tools-v0.1.2...find-msvc-tools-v0.1.3) Updates `flate2` from 1.1.2 to 1.1.4 - [Release notes](https://github.com/rust-lang/flate2-rs/releases) - [Commits](https://github.com/rust-lang/flate2-rs/compare/1.1.2...1.1.4) Updates `gimli` from 0.31.1 to 0.32.3 - [Changelog](https://github.com/gimli-rs/gimli/blob/master/CHANGELOG.md) - [Commits](https://github.com/gimli-rs/gimli/compare/0.31.1...0.32.3) Updates `libc` from 0.2.175 to 0.2.176 - [Release notes](https://github.com/rust-lang/libc/releases) - [Changelog](https://github.com/rust-lang/libc/blob/0.2.176/CHANGELOG.md) - [Commits](https://github.com/rust-lang/libc/compare/0.2.175...0.2.176) Updates `lock_api` from 0.4.13 to 0.4.14 - [Release notes](https://github.com/Amanieu/parking_lot/releases) - [Changelog](https://github.com/Amanieu/parking_lot/blob/master/CHANGELOG.md) - [Commits](https://github.com/Amanieu/parking_lot/compare/lock_api-v0.4.13...lock_api-v0.4.14) Updates `memchr` from 2.7.5 to 2.7.6 - [Commits](https://github.com/BurntSushi/memchr/compare/2.7.5...2.7.6) Updates `miniserde` from 0.1.42 to 0.1.43 - [Release notes](https://github.com/dtolnay/miniserde/releases) - [Commits](https://github.com/dtolnay/miniserde/compare/0.1.42...0.1.43) Updates `munge` from 0.4.6 to 0.4.7 - [Release notes](https://github.com/djkoloski/munge/releases) - [Commits](https://github.com/djkoloski/munge/commits) Updates `munge_macro` from 0.4.6 to 0.4.7 - [Release notes](https://github.com/djkoloski/munge/releases) - [Commits](https://github.com/djkoloski/munge/commits) Updates `object` from 0.36.7 to 0.37.3 - [Changelog](https://github.com/gimli-rs/object/blob/master/CHANGELOG.md) - [Commits](https://github.com/gimli-rs/object/compare/0.36.7...0.37.3) Updates `parking_lot_core` from 0.9.11 to 0.9.12 - [Release notes](https://github.com/Amanieu/parking_lot/releases) - [Changelog](https://github.com/Amanieu/parking_lot/blob/master/CHANGELOG.md) - [Commits](https://github.com/Amanieu/parking_lot/compare/parking_lot_core-v0.9.11...parking_lot_core-v0.9.12) Updates `ptr_meta` from 0.3.0 to 0.3.1 - [Release notes](https://github.com/rkyv/ptr_meta/releases) - [Changelog](https://github.com/rkyv/ptr_meta/blob/main/CHANGELOG.md) - [Commits](https://github.com/rkyv/ptr_meta/commits) Updates `ptr_meta_derive` from 0.3.0 to 0.3.1 - [Release notes](https://github.com/rkyv/ptr_meta/releases) - [Changelog](https://github.com/rkyv/ptr_meta/blob/main/CHANGELOG.md) - [Commits](https://github.com/rkyv/ptr_meta/commits) Updates `rancor` from 0.1.0 to 0.1.1 - [Release notes](https://github.com/rkyv/rancor/releases) - [Commits](https://github.com/rkyv/rancor/commits) Updates `redox_syscall` from 0.5.17 to 0.5.18 Updates `regex-automata` from 0.4.10 to 0.4.11 - [Release notes](https://github.com/rust-lang/regex/releases) - [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-lang/regex/commits) Updates `rend` from 0.5.2 to 0.5.3 - [Release notes](https://github.com/djkoloski/rend/releases) - [Commits](https://github.com/djkoloski/rend/commits) Updates `rkyv_derive` from 0.8.11 to 0.8.12 - [Release notes](https://github.com/rkyv/rkyv/releases) - [Commits](https://github.com/rkyv/rkyv/commits) Updates `rustls-webpki` from 0.103.6 to 0.103.7 - [Release notes](https://github.com/rustls/webpki/releases) - [Commits](https://github.com/rustls/webpki/compare/v/0.103.6...v/0.103.7) Updates `tokio-rustls` from 0.26.3 to 0.26.4 - [Release notes](https://github.com/rustls/tokio-rustls/releases) - [Commits](https://github.com/rustls/tokio-rustls/compare/v/0.26.3...v/0.26.4) Updates `tungstenite` from 0.26.2 to 0.27.0 - [Changelog](https://github.com/snapview/tungstenite-rs/blob/master/CHANGELOG.md) - [Commits](https://github.com/snapview/tungstenite-rs/compare/v0.26.2...v0.27.0) Updates `typenum` from 1.18.0 to 1.19.0 - [Release notes](https://github.com/paholg/typenum/releases) - [Changelog](https://github.com/paholg/typenum/blob/main/CHANGELOG.md) - [Commits](https://github.com/paholg/typenum/compare/v1.18.0...v1.19.0) Updates `zeroize` from 1.8.1 to 1.8.2 - [Commits](https://github.com/RustCrypto/utils/compare/zeroize-v1.8.1...zeroize-v1.8.2) --- updated-dependencies: - dependency-name: thiserror dependency-version: 2.0.17 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: const_format dependency-version: 0.2.35 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: parking_lot dependency-version: 0.12.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: axum dependency-version: 0.8.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: quote dependency-version: 1.0.41 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: tokio-tungstenite dependency-version: 0.28.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: rust-dependencies - dependency-name: camino dependency-version: 1.2.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: rkyv dependency-version: 0.8.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: regex dependency-version: 1.11.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: tempfile dependency-version: 3.23.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: rust-dependencies - dependency-name: attribute-derive dependency-version: 0.10.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: actix-http dependency-version: 3.11.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: attribute-derive-macro dependency-version: 0.10.5 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: axum-core dependency-version: 0.5.5 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: backtrace dependency-version: 0.3.76 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: bytecheck dependency-version: 0.8.2 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: bytecheck_derive dependency-version: 0.8.2 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: cc dependency-version: 1.2.40 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: collection_literals dependency-version: 1.0.3 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: deranged dependency-version: 0.5.4 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: find-msvc-tools dependency-version: 0.1.3 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: flate2 dependency-version: 1.1.4 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: gimli dependency-version: 0.32.3 dependency-type: indirect update-type: version-update:semver-minor dependency-group: rust-dependencies - dependency-name: libc dependency-version: 0.2.176 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: lock_api dependency-version: 0.4.14 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: memchr dependency-version: 2.7.6 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: miniserde dependency-version: 0.1.43 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: munge dependency-version: 0.4.7 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: munge_macro dependency-version: 0.4.7 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: object dependency-version: 0.37.3 dependency-type: indirect update-type: version-update:semver-minor dependency-group: rust-dependencies - dependency-name: parking_lot_core dependency-version: 0.9.12 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: ptr_meta dependency-version: 0.3.1 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: ptr_meta_derive dependency-version: 0.3.1 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: rancor dependency-version: 0.1.1 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: redox_syscall dependency-version: 0.5.18 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: regex-automata dependency-version: 0.4.11 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: rend dependency-version: 0.5.3 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: rkyv_derive dependency-version: 0.8.12 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: rustls-webpki dependency-version: 0.103.7 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: tokio-rustls dependency-version: 0.26.4 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: tungstenite dependency-version: 0.27.0 dependency-type: indirect update-type: version-update:semver-minor dependency-group: rust-dependencies - dependency-name: typenum dependency-version: 1.19.0 dependency-type: indirect update-type: version-update:semver-minor dependency-group: rust-dependencies - dependency-name: zeroize dependency-version: 1.8.2 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 272 ++++++++++++++++++++++++++--------------------------- Cargo.toml | 24 ++--- 2 files changed, 144 insertions(+), 152 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2af34ca4a5..01fff57fef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,9 +44,9 @@ dependencies = [ [[package]] name = "actix-http" -version = "3.11.1" +version = "3.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44cceded2fb55f3c4b67068fa64962e2ca59614edc5b03167de9ff82ae803da0" +checksum = "7926860314cbe2fb5d1f13731e387ab43bd32bca224e82e6e2db85de0a3dba49" dependencies = [ "actix-codec", "actix-rt", @@ -224,9 +224,9 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.24.2" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ "gimli", ] @@ -270,7 +270,7 @@ dependencies = [ "futures-lite", "glib", "serial_test", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tracing", "wasm-bindgen-futures", @@ -370,9 +370,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "attribute-derive" -version = "0.10.3" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0053e96dd3bec5b4879c23a138d6ef26f2cb936c9cdc96274ac2b9ed44b5bb54" +checksum = "05832cdddc8f2650cc2cc187cc2e952b8c133a48eb055f35211f61ee81502d77" dependencies = [ "attribute-derive-macro", "derive-where", @@ -384,9 +384,9 @@ dependencies = [ [[package]] name = "attribute-derive-macro" -version = "0.10.3" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "463b53ad0fd5b460af4b1915fe045ff4d946d025fb6c4dc3337752eaa980f71b" +checksum = "0a7cdbbd4bd005c5d3e2e9c885e6fa575db4f4a3572335b974d8db853b6beb61" dependencies = [ "collection_literals", "interpolator", @@ -406,9 +406,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "axum" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" +checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" dependencies = [ "axum-core", "base64", @@ -427,15 +427,14 @@ dependencies = [ "multer", "percent-encoding", "pin-project-lite", - "rustversion", - "serde", + "serde_core", "serde_json", "serde_path_to_error", "serde_urlencoded", "sha1", "sync_wrapper", "tokio", - "tokio-tungstenite 0.26.2", + "tokio-tungstenite", "tower", "tower-layer", "tower-service", @@ -444,9 +443,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.2" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" +checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" dependencies = [ "bytes", "futures-core", @@ -455,7 +454,6 @@ dependencies = [ "http-body-util", "mime", "pin-project-lite", - "rustversion", "sync_wrapper", "tower-layer", "tower-service", @@ -464,9 +462,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.75" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" dependencies = [ "addr2line", "cfg-if", @@ -474,7 +472,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-targets 0.52.6", + "windows-link 0.2.0", ] [[package]] @@ -533,9 +531,9 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytecheck" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50690fb3370fb9fe3550372746084c46f2ac8c9685c583d2be10eefd89d3d1a3" +checksum = "0caa33a2c0edca0419d15ac723dff03f1956f7978329b1e3b5fdaaaed9d3ca8b" dependencies = [ "bytecheck_derive", "ptr_meta", @@ -545,9 +543,9 @@ dependencies = [ [[package]] name = "bytecheck_derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efb7846e0cb180355c2dec69e721edafa36919850f1a9f52ffba4ebc0393cb71" +checksum = "89385e82b5d1821d2219e0b095efa2cc1f246cbf99080f3be46a1a85c0d392d9" dependencies = [ "proc-macro2", "quote", @@ -577,15 +575,15 @@ dependencies = [ [[package]] name = "camino" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1de8bc0aa9e9385ceb3bf0c152e3a9b9544f6c4a912c8ae504e80c1f0368603" +checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" [[package]] name = "cc" -version = "1.2.38" +version = "1.2.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80f41ae168f955c12fb8960b057d70d0ca153fb83182b57d86380443527be7e9" +checksum = "e1d05d92f4b1fd76aad469d46cdd858ca761576082cd37df81416691e50199fb" dependencies = [ "find-msvc-tools", "jobserver", @@ -648,7 +646,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" dependencies = [ - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -664,15 +662,15 @@ dependencies = [ "serde-lite", "serde-wasm-bindgen", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", "wasm-bindgen", ] [[package]] name = "collection_literals" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b3f65b8fb8e88ba339f7d23a390fe1b0896217da05e2a66c584c9b29a91df8" +checksum = "2550f75b8cfac212855f6b1885455df8eaee8fe8e246b647d69146142e016084" [[package]] name = "concurrent-queue" @@ -716,9 +714,9 @@ checksum = "451d0640545a0553814b4c646eb549343561618838e9b42495f466131fe3ad49" [[package]] name = "const_format" -version = "0.2.34" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" dependencies = [ "const_format_proc_macros", ] @@ -851,9 +849,9 @@ checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "deranged" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" +checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" dependencies = [ "powerfmt", ] @@ -947,7 +945,7 @@ dependencies = [ "serde", "serde_json", "subsecond", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", "tungstenite 0.27.0", "warnings", @@ -1057,7 +1055,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.0", ] [[package]] @@ -1089,15 +1087,15 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "find-msvc-tools" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" +checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3" [[package]] name = "flate2" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" dependencies = [ "crc32fast", "miniz_oxide", @@ -1291,9 +1289,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.31.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] name = "gio-sys" @@ -1655,7 +1653,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.0", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -1912,7 +1910,7 @@ dependencies = [ "slotmap", "subsecond", "tachys", - "thiserror 2.0.16", + "thiserror 2.0.17", "throw_error", "tokio", "tokio-test", @@ -1994,7 +1992,7 @@ dependencies = [ "serde", "temp-env", "tempfile", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "typed-builder", ] @@ -2106,7 +2104,7 @@ dependencies = [ "rustc_version", "send_wrapper", "tachys", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", "url", "wasm-bindgen", @@ -2149,9 +2147,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.175" +version = "0.2.176" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" [[package]] name = "libloading" @@ -2200,11 +2198,10 @@ checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] @@ -2257,9 +2254,9 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "memchr" -version = "2.7.5" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memfd" @@ -2297,9 +2294,9 @@ dependencies = [ [[package]] name = "mini-internal" -version = "0.1.42" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6c74ab4f1a0c0ab045260ee4727b23c00cc17e5eff5095262d08eef8c3c8d49" +checksum = "560f32b6891d8d9bade8942c45a27694f16d789d3b4b8e6b7135a5240de0a8af" dependencies = [ "proc-macro2", "quote", @@ -2318,9 +2315,9 @@ dependencies = [ [[package]] name = "miniserde" -version = "0.1.42" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08ec68bf2ad170a53a6efa92c3df7187968d6e475fe7a935725868154074ca0f" +checksum = "ac79f4123c070de643a7a93b9339abf18c30005c622bf4b1c29c2c0960f52d39" dependencies = [ "itoa", "mini-internal", @@ -2334,6 +2331,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -2367,18 +2365,18 @@ dependencies = [ [[package]] name = "munge" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7feb0b48aa0a25f9fe0899482c6e1379ee7a11b24a53073eacdecb9adb6dc60" +checksum = "5e17401f259eba956ca16491461b6e8f72913a0a114e39736ce404410f915a0c" dependencies = [ "munge_macro", ] [[package]] name = "munge_macro" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2e3795a5d2da581a8b252fec6022eee01aea10161a4d1bf237d4cbe47f7e988" +checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931" dependencies = [ "proc-macro2", "quote", @@ -2433,9 +2431,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.7" +version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" dependencies = [ "memchr", ] @@ -2446,7 +2444,7 @@ version = "0.2.1" dependencies = [ "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -2511,9 +2509,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -2521,15 +2519,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-link 0.2.0", ] [[package]] @@ -2725,18 +2723,18 @@ dependencies = [ [[package]] name = "ptr_meta" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe9e76f66d3f9606f44e45598d155cb13ecf09f4a28199e48daf8c8fc937ea90" +checksum = "0b9a0cf95a1196af61d4f1cbdab967179516d9a4a4312af1f31948f8f6224a79" dependencies = [ "ptr_meta_derive", ] [[package]] name = "ptr_meta_derive" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca414edb151b4c8d125c12566ab0d74dc9cdba36fb80eb7b848c15f495fd32d1" +checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1" dependencies = [ "proc-macro2", "quote", @@ -2756,8 +2754,8 @@ dependencies = [ "quinn-udp", "rustc-hash 2.1.1", "rustls", - "socket2 0.6.0", - "thiserror 2.0.16", + "socket2 0.5.10", + "thiserror 2.0.17", "tokio", "tracing", "web-time", @@ -2778,7 +2776,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.16", + "thiserror 2.0.17", "tinyvec", "tracing", "web-time", @@ -2793,16 +2791,16 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.0", + "socket2 0.5.10", "tracing", "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" dependencies = [ "proc-macro2", ] @@ -2837,9 +2835,9 @@ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rancor" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf5f7161924b9d1cea0e4cabc97c372cea92b5f927fc13c6bca67157a0ad947" +checksum = "a063ea72381527c2a0561da9c80000ef822bdd7c3241b1cc1b12100e3df081ee" dependencies = [ "ptr_meta", ] @@ -2891,7 +2889,7 @@ dependencies = [ "serde", "slotmap", "subsecond", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tokio-test", "tracing", @@ -2930,18 +2928,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.17" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags", ] [[package]] name = "regex" -version = "1.11.2" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" dependencies = [ "aho-corasick", "memchr", @@ -2951,9 +2949,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" dependencies = [ "aho-corasick", "memchr", @@ -2974,9 +2972,9 @@ checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] name = "rend" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a35e8a6bf28cd121053a66aa2e6a2e3eaffad4a60012179f0e864aa5ffeff215" +checksum = "cadadef317c2f20755a64d7fdc48f9e7178ee6b0e1f7fce33fa60f1d68a276e6" dependencies = [ "bytecheck", ] @@ -3042,9 +3040,9 @@ dependencies = [ [[package]] name = "rkyv" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f5c3e5da784cd8c69d32cdc84673f3204536ca56e1fa01be31a74b92c932ac" +checksum = "35a640b26f007713818e9a9b65d34da1cf58538207b052916a83d80e43f3ffa4" dependencies = [ "bytecheck", "bytes", @@ -3061,9 +3059,9 @@ dependencies = [ [[package]] name = "rkyv_derive" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4270433626cffc9c4c1d3707dd681f2a2718d3d7b09ad754bec137acecda8d22" +checksum = "bd83f5f173ff41e00337d97f6572e416d022ef8a19f371817259ae960324c482" dependencies = [ "proc-macro2", "quote", @@ -3104,7 +3102,7 @@ dependencies = [ "quote", "syn 2.0.106", "syn_derive", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -3144,7 +3142,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.0", ] [[package]] @@ -3173,9 +3171,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.6" +version = "0.103.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8572f3c2cb9934231157b45499fc41e1f58c589fdfb81a844ba873265e80f8eb" +checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" dependencies = [ "ring", "rustls-pki-types", @@ -3365,7 +3363,7 @@ checksum = "f3faaf9e727533a19351a43cc5a8de957372163c7d35cc48c90b75cdda13c352" dependencies = [ "percent-encoding", "serde", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -3457,10 +3455,10 @@ dependencies = [ "serde_json", "serde_qs", "server_fn_macro_default", - "thiserror 2.0.16", + "thiserror 2.0.17", "throw_error", "tokio", - "tokio-tungstenite 0.27.0", + "tokio-tungstenite", "tower", "tower-layer", "trybuild", @@ -3544,6 +3542,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "simdutf8" version = "0.1.5" @@ -3655,7 +3659,7 @@ dependencies = [ "memmap2", "serde", "subsecond-types", - "thiserror 2.0.16", + "thiserror 2.0.17", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -3805,15 +3809,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.22.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.0", ] [[package]] @@ -3836,11 +3840,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.16", + "thiserror-impl 2.0.17", ] [[package]] @@ -3856,9 +3860,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", @@ -3971,9 +3975,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.3" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f63835928ca123f1bef57abbcd23bb2ba0ac9ae1235f1e65bda0d06e7786bd" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", "tokio", @@ -4005,27 +4009,15 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.26.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" -dependencies = [ - "futures-util", - "log", - "tokio", - "tungstenite 0.26.2", -] - -[[package]] -name = "tokio-tungstenite" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "489a59b6730eda1b0171fcfda8b121f4bee2b35cba8645ca35c5f7ba3eb736c1" +checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" dependencies = [ "futures-util", "log", "rustls", "tokio", - "tungstenite 0.27.0", + "tungstenite 0.28.0", ] [[package]] @@ -4237,9 +4229,9 @@ dependencies = [ [[package]] name = "tungstenite" -version = "0.26.2" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +checksum = "eadc29d668c91fcc564941132e17b28a7ceb2f3ebf0b9dae3e03fd7a6748eb0d" dependencies = [ "bytes", "data-encoding", @@ -4248,15 +4240,15 @@ dependencies = [ "log", "rand", "sha1", - "thiserror 2.0.16", + "thiserror 2.0.17", "utf-8", ] [[package]] name = "tungstenite" -version = "0.27.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadc29d668c91fcc564941132e17b28a7ceb2f3ebf0b9dae3e03fd7a6748eb0d" +checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" dependencies = [ "bytes", "data-encoding", @@ -4265,7 +4257,7 @@ dependencies = [ "log", "rand", "sha1", - "thiserror 2.0.16", + "thiserror 2.0.17", "utf-8", ] @@ -4291,9 +4283,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicase" @@ -4620,7 +4612,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.0", ] [[package]] @@ -4900,9 +4892,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" diff --git a/Cargo.toml b/Cargo.toml index a86392695b..aeea28e3f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,7 +80,7 @@ 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" } -thiserror = { default-features = false, version = "2.0.16" } +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" } rstml = { default-features = false, version = "0.12.1" } @@ -97,7 +97,7 @@ send_wrapper = { default-features = false, version = "0.6.0" } tokio-test = { default-features = false, version = "0.4.4" } html-escape = { default-features = false, version = "0.2.13" } proc-macro-error2 = { default-features = false, version = "2.0.1" } -const_format = { default-features = false, version = "0.2.34" } +const_format = { default-features = false, version = "0.2.35" } gloo-net = { default-features = false, version = "0.6.0" } url = { default-features = false, version = "2.5.4" } tokio = { default-features = false, version = "1.47.1" } @@ -107,18 +107,18 @@ wasm-bindgen-futures = { default-features = false, version = "0.4.50" } tower = { default-features = false, version = "0.5.2" } proc-macro2 = { default-features = false, version = "1.0.101" } serde = { default-features = false, version = "1.0.219" } -parking_lot = { default-features = false, version = "0.12.4" } -axum = { default-features = false, version = "0.8.4" } +parking_lot = { default-features = false, version = "0.12.5" } +axum = { default-features = false, version = "0.8.6" } serde_qs = { default-features = false, version = "0.15.0" } syn = { default-features = false, version = "2.0.106" } xxhash-rust = { default-features = false, version = "0.8.15" } paste = { default-features = false, version = "1.0.15" } -quote = { default-features = false, version = "1.0.40" } +quote = { default-features = false, version = "1.0.41" } web-sys = { default-features = false, version = "0.3.77" } js-sys = { default-features = false, version = "0.3.77" } rand = { default-features = false, version = "0.9.1" } serde-lite = { default-features = false, version = "0.5.0" } -tokio-tungstenite = { default-features = false, version = "0.27.0" } +tokio-tungstenite = { default-features = false, version = "0.28.0" } 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" } @@ -132,21 +132,21 @@ tower-http = { default-features = false, version = "0.6.4" } prettyplease = { default-features = false, version = "0.2.37" } inventory = { default-features = false, version = "0.3.21" } config = { default-features = false, version = "0.15.14" } -camino = { default-features = false, version = "1.1.11" } +camino = { default-features = false, version = "1.2.1" } ciborium = { default-features = false, version = "0.2.2" } multer = { default-features = false, version = "3.1.0" } leptos-spin-macro = { default-features = false, version = "0.2.0" } sledgehammer_utils = { default-features = false, version = "0.3.1" } sledgehammer_bindgen = { default-features = false, version = "0.6.0" } wasm-streams = { default-features = false, version = "0.4.2" } -rkyv = { default-features = false, version = "0.8.11" } +rkyv = { default-features = false, version = "0.8.12" } temp-env = { default-features = false, version = "0.3.6" } uuid = { default-features = false, version = "1.18.0" } bytes = { default-features = false, version = "1.10.1" } http = { default-features = false, version = "1.3.1" } -regex = { default-features = false, version = "1.11.2" } +regex = { default-features = false, version = "1.11.3" } drain_filter_polyfill = { default-features = false, version = "0.1.3" } -tempfile = { default-features = false, version = "3.21.0" } +tempfile = { default-features = false, version = "3.23.0" } futures-lite = { default-features = false, version = "2.6.1" } log = { default-features = false, version = "0.4.27" } percent-encoding = { default-features = false, version = "2.3.2" } @@ -158,10 +158,10 @@ postcard = { default-features = false, version = "1.1.3" } rmp-serde = { default-features = false, version = "1.3.0" } reqwest = { default-features = false, version = "0.12.23" } tower-layer = { default-features = false, version = "0.3.3" } -attribute-derive = { default-features = false, version = "0.10.3" } +attribute-derive = { default-features = false, version = "0.10.5" } insta = { default-features = false, version = "1.43.1" } codee = { default-features = false, version = "0.3.0" } -actix-http = { default-features = false, version = "3.11.1" } +actix-http = { default-features = false, version = "3.11.2" } wasm-bindgen-test = { default-features = false, version = "0.3.50" } rustversion = { default-features = false, version = "1.0.22" } getrandom = { default-features = false, version = "0.3.3" } From 5c6f5ce8e9089626d45bc8e21b56314dc2b41776 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Fri, 10 Oct 2025 11:26:07 -0400 Subject: [PATCH 51/91] fix: allow setting `NodeRef` by implementing `IsDisposed` (#4367) --- tachys/src/reactive_graph/node_ref.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tachys/src/reactive_graph/node_ref.rs b/tachys/src/reactive_graph/node_ref.rs index 8ed0a0eaa3..2f952a5f8e 100644 --- a/tachys/src/reactive_graph/node_ref.rs +++ b/tachys/src/reactive_graph/node_ref.rs @@ -7,8 +7,8 @@ use reactive_graph::{ RwSignal, }, traits::{ - DefinedAt, Get, Notify, ReadUntracked, Set, Track, UntrackableGuard, - Write, + DefinedAt, Get, IsDisposed, Notify, ReadUntracked, Set, Track, + UntrackableGuard, Write, }, }; use send_wrapper::SendWrapper; @@ -158,6 +158,16 @@ where } } +impl<E> IsDisposed for NodeRef<E> +where + E: ElementType, + E::Output: 'static, +{ + fn is_disposed(&self) -> bool { + self.0.is_disposed() + } +} + /// Create a [NodeRef]. #[inline(always)] #[track_caller] From dcf84cab61c5504fc571c532eeab30356279ef5b Mon Sep 17 00:00:00 2001 From: mahdi739 <86552031+mahdi739@users.noreply.github.com> Date: Fri, 10 Oct 2025 18:56:54 +0330 Subject: [PATCH 52/91] chore: re-export `debug_log` and `debug_error` in logging module (#4335) * chore: re-export `debug_log` and `debug_error` in logging module * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- leptos/src/lib.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/leptos/src/lib.rs b/leptos/src/lib.rs index 274a6cf34a..c97d185d30 100644 --- a/leptos/src/lib.rs +++ b/leptos/src/lib.rs @@ -311,7 +311,9 @@ pub mod subsecond; /// Utilities for simple isomorphic logging to the console or terminal. pub mod logging { - pub use leptos_dom::{debug_warn, error, log, warn}; + pub use leptos_dom::{ + debug_error, debug_log, debug_warn, error, log, warn, + }; } /// Utilities for working with asynchronous tasks. From 8091affb8e90c5f3b22d7268b301c0698bd319a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20B=C3=BCsch?= <antoine.busch@gmail.com> Date: Sat, 11 Oct 2025 02:29:23 +1100 Subject: [PATCH 53/91] feat: allow `anyhow::Error` (and similar types) to be converted to `throw_error::Error` (#4359) * Allow more types to be converted to throw_error::Error ## Context At the moment it is quite difficult to get crates like `anyhow` to play well with leptos, in particular because it is _very_ difficult to convert an `anyhow::Error` type to a `leptos::Error` type. This is because the only way to construct a `leptos::Error` currently is via its `From` implementation, which exists for any type that implements the standard `Error` trait, but `anyhow::Error` does not implement `StdError` directly. It can however be converted to a boxed trait object via its [`.into_boxed_dyn_error()`][4] method, and you would think that `Box<dyn Error>` implements `Error`, but [that is sadly not the case][1]. ## Solution Change the blanket implementation of `From` for `throw_error::Error` from "any type that implements the Error trait" to "any type that can be converted to a boxed Error trait object". This works because: - A `Box` [can be converted][2] to an `Arc` for any type (including unsized types like trait objects), - Any type that implements the standard `Error` trait [can be converted][3] to a `Box<dyn Error>`, so the new `From` blanket implementation covers a strict superset of what was previously allowed. This change now allows types like `anyhow::Error`, but also `String`, to be easily converted to a leptos `Error`, therefore making them play well with things like `<ErrorBoundary>`. [1]: https://stackoverflow.com/questions/65151237/why-doesnt-boxdyn-error-implement-error [2]: https://doc.rust-lang.org/stable/std/sync/struct.Arc.html#impl-From%3CBox%3CT,+A%3E%3E-for-Arc%3CT,+A%3E [3]: https://doc.rust-lang.org/stable/std/error/trait.Error.html#impl-From%3CE%3E-for-Box%3Cdyn+Error%3E [4]: https://docs.rs/anyhow/latest/anyhow/struct.Error.html#method.into_boxed_dyn_error * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- Cargo.lock | 1 + any_error/Cargo.toml | 3 +++ any_error/src/lib.rs | 33 +++++++++++++++++++++++++++++++-- 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 01fff57fef..f85b2199af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3873,6 +3873,7 @@ dependencies = [ name = "throw_error" version = "0.3.0" dependencies = [ + "anyhow", "pin-project-lite", ] diff --git a/any_error/Cargo.toml b/any_error/Cargo.toml index 8765ef80ec..2650b3b394 100644 --- a/any_error/Cargo.toml +++ b/any_error/Cargo.toml @@ -11,3 +11,6 @@ edition.workspace = true [dependencies] pin-project-lite = { workspace = true, default-features = true } + +[dev-dependencies] +anyhow.workspace = true diff --git a/any_error/src/lib.rs b/any_error/src/lib.rs index eee61c15a1..39a7d9f8ed 100644 --- a/any_error/src/lib.rs +++ b/any_error/src/lib.rs @@ -45,10 +45,10 @@ impl fmt::Display for Error { impl<T> From<T> for Error where - T: error::Error + Send + Sync + 'static, + T: Into<Box<dyn error::Error + Send + Sync + 'static>>, { fn from(value: T) -> Self { - Error(Arc::new(value)) + Error(Arc::from(value.into())) } } @@ -158,3 +158,32 @@ where this.inner.poll(cx) } } + +#[cfg(test)] +mod tests { + use super::*; + use std::error::Error as StdError; + + #[derive(Debug)] + struct MyError; + + impl Display for MyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "MyError") + } + } + + impl StdError for MyError {} + + #[test] + fn test_from() { + let e = MyError; + let _le = Error::from(e); + + let e = "some error".to_string(); + let _le = Error::from(e); + + let e = anyhow::anyhow!("anyhow error"); + let _le = Error::from(e); + } +} From 4f12a8a40e1f461d878a8d7565c955b4b02357db Mon Sep 17 00:00:00 2001 From: QuartzLibrary <81446760+QuartzLibrary@users.noreply.github.com> Date: Sat, 27 Sep 2025 21:41:01 +0100 Subject: [PATCH 54/91] feat: `ImmediateEffect::new_mut_scoped` --- reactive_graph/src/effect/immediate.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/reactive_graph/src/effect/immediate.rs b/reactive_graph/src/effect/immediate.rs index b9f752bb01..0f4e49ea23 100644 --- a/reactive_graph/src/effect/immediate.rs +++ b/reactive_graph/src/effect/immediate.rs @@ -95,6 +95,7 @@ impl ImmediateEffect { /// Creates a new effect which runs immediately, then again as soon as any tracked signal changes. /// /// NOTE: this requires a `Fn` function because it might recurse. + /// Use [Self::new_mut_scoped] to pass a `FnMut` function, it'll panic on recursion. /// NOTE: this effect is automatically cleaned up when the current owner is cleared or disposed. #[track_caller] pub fn new_scoped(fun: impl Fn() + Send + Sync + 'static) { @@ -102,6 +103,18 @@ impl ImmediateEffect { on_cleanup(move || effect.dispose()); } + /// Creates a new effect which runs immediately, then again as soon as any tracked signal changes. + /// + /// NOTE: this effect is automatically cleaned up when the current owner is cleared or disposed. + /// + /// # Panics + /// Panics on recursion or if triggered in parallel. Also see [Self::new_scoped] + #[track_caller] + pub fn new_mut_scoped(fun: impl FnMut() + Send + Sync + 'static) { + let effect = Self::new_mut(fun); + + on_cleanup(move || effect.dispose()); + } /// Creates a new effect which runs immediately, then again as soon as any tracked signal changes. /// From 365250f160eb929b3f3b62034835796eca70c892 Mon Sep 17 00:00:00 2001 From: QuartzLibrary <81446760+QuartzLibrary@users.noreply.github.com> Date: Sat, 27 Sep 2025 21:42:31 +0100 Subject: [PATCH 55/91] fix: `ImmediateEffect` debug info --- reactive_graph/src/effect/immediate.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/reactive_graph/src/effect/immediate.rs b/reactive_graph/src/effect/immediate.rs index 0f4e49ea23..ee82aa34ce 100644 --- a/reactive_graph/src/effect/immediate.rs +++ b/reactive_graph/src/effect/immediate.rs @@ -215,6 +215,8 @@ mod inner { fun: impl Fn() + Send + Sync + 'static, ) -> Arc<RwLock<EffectInner>> { let owner = Owner::new(); + #[cfg(any(debug_assertions, leptos_debuginfo))] + let defined_at = Location::caller(); Arc::new_cyclic(|weak| { let any_subscriber = AnySubscriber( @@ -224,7 +226,7 @@ mod inner { RwLock::new(EffectInner { #[cfg(any(debug_assertions, leptos_debuginfo))] - defined_at: Location::caller(), + defined_at, owner, state: ReactiveNodeState::Dirty, run_count_start: 0, From 00599bc5826fd922af0458ca4814d135a26828ba Mon Sep 17 00:00:00 2001 From: QuartzLibrary <81446760+QuartzLibrary@users.noreply.github.com> Date: Sat, 27 Sep 2025 22:48:28 +0100 Subject: [PATCH 56/91] feat: `effect::immediate::batch` --- reactive_graph/src/effect/immediate.rs | 56 ++++++++++++++++++++++++ reactive_graph/tests/effect_immediate.rs | 35 +++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/reactive_graph/src/effect/immediate.rs b/reactive_graph/src/effect/immediate.rs index ee82aa34ce..f0d01c3404 100644 --- a/reactive_graph/src/effect/immediate.rs +++ b/reactive_graph/src/effect/immediate.rs @@ -65,6 +65,7 @@ impl Dispose for ImmediateEffect { impl ImmediateEffect { /// Creates a new effect which runs immediately, then again as soon as any tracked signal changes. + /// (Unless [batch] is used.) /// /// NOTE: this requires a `Fn` function because it might recurse. /// Use [Self::new_mut] to pass a `FnMut` function, it'll panic on recursion. @@ -82,6 +83,7 @@ impl ImmediateEffect { Self { inner: Some(inner) } } /// Creates a new effect which runs immediately, then again as soon as any tracked signal changes. + /// (Unless [batch] is used.) /// /// # Panics /// Panics on recursion or if triggered in parallel. Also see [Self::new] @@ -93,6 +95,7 @@ impl ImmediateEffect { Self::new(move || fun.try_lock().expect(MSG)()) } /// Creates a new effect which runs immediately, then again as soon as any tracked signal changes. + /// (Unless [batch] is used.) /// /// NOTE: this requires a `Fn` function because it might recurse. /// Use [Self::new_mut_scoped] to pass a `FnMut` function, it'll panic on recursion. @@ -104,6 +107,7 @@ impl ImmediateEffect { on_cleanup(move || effect.dispose()); } /// Creates a new effect which runs immediately, then again as soon as any tracked signal changes. + /// (Unless [batch] is used.) /// /// NOTE: this effect is automatically cleaned up when the current owner is cleared or disposed. /// @@ -143,6 +147,41 @@ impl DefinedAt for ImmediateEffect { } } +/// Defers any [ImmediateEffect]s from running until the end of the function. +/// +/// NOTE: this affects only [ImmediateEffect]s, not other effects. +/// +/// NOTE: this is rarely needed, but it is useful for example when multiple signals +/// need to be updated atomically (for example a double-bound signal tree). +pub fn batch<T>(f: impl FnOnce() -> T) -> T { + struct ExecuteOnDrop; + impl Drop for ExecuteOnDrop { + fn drop(&mut self) { + let effects = { + let mut batch = inner::BATCH.write().or_poisoned(); + batch.take().unwrap().into_inner().expect("lock poisoned") + }; + // TODO: Should we skip the effects if it's panicking? + for effect in effects { + effect.update_if_necessary(); + } + } + } + let mut execute_on_drop = None; + { + let mut batch = inner::BATCH.write().or_poisoned(); + if batch.is_none() { + execute_on_drop = Some(ExecuteOnDrop); + } else { + // Nested batching has no effect. + } + *batch = Some(batch.take().unwrap_or_default()); + } + let ret = f(); + drop(execute_on_drop); + ret +} + mod inner { use crate::{ graph::{ @@ -153,6 +192,7 @@ mod inner { owner::Owner, traits::DefinedAt, }; + use indexmap::IndexSet; use or_poisoned::OrPoisoned; use std::{ panic::Location, @@ -160,6 +200,11 @@ mod inner { thread::{self, ThreadId}, }; + /// Only the [super::batch] function ever writes to the outer RwLock. + /// While the effects will write to the inner one. + pub(super) static BATCH: RwLock<Option<RwLock<IndexSet<AnySubscriber>>>> = + RwLock::new(None); + /// Handles subscription logic for effects. /// /// To handle parallelism and recursion we assign ordered (1..) ids to each run. @@ -275,6 +320,17 @@ mod inner { ReactiveNodeState::Dirty => true, }; + { + if let Some(batch) = &*BATCH.read().or_poisoned() { + let mut batch = batch.write().or_poisoned(); + let subscriber = + self.read().or_poisoned().any_subscriber.clone(); + + batch.insert(subscriber); + return needs_update; + } + } + if needs_update { let mut guard = self.write().or_poisoned(); diff --git a/reactive_graph/tests/effect_immediate.rs b/reactive_graph/tests/effect_immediate.rs index a5f1cfef63..d0502deafa 100644 --- a/reactive_graph/tests/effect_immediate.rs +++ b/reactive_graph/tests/effect_immediate.rs @@ -225,3 +225,38 @@ fn threaded_chaos_effect() { let values: Vec<_> = signals.iter().map(|s| s.get_untracked()).collect(); println!("FINAL: {values:?}"); } + +#[cfg(feature = "effects")] +#[test] +fn test_batch() { + use imports::*; + use reactive_graph::{effect::batch, owner::StoredValue}; + + let owner = Owner::new(); + owner.set(); + + let a = RwSignal::new(0); + let b = RwSignal::new(0); + + let values = StoredValue::new(Vec::new()); + + ImmediateEffect::new_scoped(move || { + println!("{} = {}", a.get(), b.get()); + values.write_value().push((a.get(), b.get())); + }); + + a.set(1); + b.set(1); + + batch(move || { + a.set(2); + b.set(2); + + batch(move || { + a.set(3); + b.set(3); + }); + }); + + assert_eq!(values.get_value(), vec![(0, 0), (1, 0), (1, 1), (3, 3)]); +} From f1efc062bdf0a09413c30b629aa484ecae4ee4c5 Mon Sep 17 00:00:00 2001 From: Michael Kadziela <67846890+Innominus@users.noreply.github.com> Date: Sat, 11 Oct 2025 03:03:31 +1100 Subject: [PATCH 57/91] fix: clean up window router events on unmount (improves subsecond support for router) Clean up window router events on unmount --- router/src/location/history.rs | 41 +++++++++++++--------------------- router/src/location/mod.rs | 7 +++--- 2 files changed, 18 insertions(+), 30 deletions(-) diff --git a/router/src/location/history.rs b/router/src/location/history.rs index 94481cf741..c7cdfc85dc 100644 --- a/router/src/location/history.rs +++ b/router/src/location/history.rs @@ -3,7 +3,7 @@ use crate::{hooks::use_navigate, params::ParamsMap}; use core::fmt; use futures::channel::oneshot; use js_sys::{try_iter, Array, JsString}; -use leptos::prelude::*; +use leptos::{ev, prelude::*}; use or_poisoned::OrPoisoned; use reactive_graph::{ signal::ArcRwSignal, @@ -11,13 +11,12 @@ use reactive_graph::{ }; use std::{ borrow::Cow, - boxed::Box, string::String, sync::{Arc, Mutex}, }; use tachys::dom::{document, window}; -use wasm_bindgen::{closure::Closure, JsCast, JsValue}; -use web_sys::{Event, UrlSearchParams}; +use wasm_bindgen::{JsCast, JsValue}; +use web_sys::UrlSearchParams; #[derive(Clone)] pub struct BrowserUrl { @@ -116,7 +115,6 @@ impl LocationProvider for BrowserUrl { } fn init(&self, base: Option<Cow<'static, str>>) { - let window = window(); let navigate = { let url = self.url.clone(); let pending = Arc::clone(&self.pending_navigation); @@ -159,27 +157,18 @@ impl LocationProvider for BrowserUrl { let handle_anchor_click = handle_anchor_click(base, Self::parse_with_base, navigate); - let closure = Closure::wrap(Box::new(move |ev: Event| { + + let click_handle = window_event_listener(ev::click, move |ev| { if let Err(e) = handle_anchor_click(ev) { #[cfg(feature = "tracing")] tracing::error!("{e:?}"); #[cfg(not(feature = "tracing"))] web_sys::console::error_1(&e); } - }) as Box<dyn FnMut(Event)>) - .into_js_value(); - window - .add_event_listener_with_callback( - "click", - closure.as_ref().unchecked_ref(), - ) - .expect( - "couldn't add `click` listener to `window` to handle `<a>` \ - clicks", - ); + }); // handle popstate event (forward/back navigation) - let cb = { + let popstate_cb = { let url = self.url.clone(); let path_stack = self.path_stack.clone(); let is_back = self.is_back.clone(); @@ -206,14 +195,14 @@ impl LocationProvider for BrowserUrl { } } }; - let closure = - Closure::wrap(Box::new(cb) as Box<dyn Fn()>).into_js_value(); - window - .add_event_listener_with_callback( - "popstate", - closure.as_ref().unchecked_ref(), - ) - .expect("couldn't add `popstate` listener to `window`"); + + let popstate_handle = + window_event_listener(ev::popstate, move |_| popstate_cb()); + + on_cleanup(|| { + click_handle.remove(); + popstate_handle.remove(); + }); } fn ready_to_complete(&self) { diff --git a/router/src/location/mod.rs b/router/src/location/mod.rs index faa361472d..d99ada30bd 100644 --- a/router/src/location/mod.rs +++ b/router/src/location/mod.rs @@ -14,7 +14,7 @@ use send_wrapper::SendWrapper; use std::{borrow::Cow, future::Future}; use tachys::dom::window; use wasm_bindgen::{JsCast, JsValue}; -use web_sys::{Event, HtmlAnchorElement, MouseEvent}; +use web_sys::{HtmlAnchorElement, MouseEvent}; mod history; mod server; @@ -300,15 +300,14 @@ pub(crate) fn handle_anchor_click<NavFn, NavFut>( router_base: Option<Cow<'static, str>>, parse_with_base: fn(&str, &str) -> Result<Url, JsValue>, navigate: NavFn, -) -> Box<dyn Fn(Event) -> Result<(), JsValue>> +) -> Box<dyn Fn(MouseEvent) -> Result<(), JsValue>> where NavFn: Fn(Url, LocationChange) -> NavFut + 'static, NavFut: Future<Output = ()> + 'static, { let router_base = router_base.unwrap_or_default(); - Box::new(move |ev: Event| { - let ev = ev.unchecked_into::<MouseEvent>(); + Box::new(move |ev: MouseEvent| { let origin = window().location().origin()?; if ev.default_prevented() || ev.button() != 0 From 68a2d22da63334b90b175ffc9cde02c2293b1f42 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Sat, 11 Oct 2025 08:04:21 -0400 Subject: [PATCH 58/91] fix: use correct/full type names for matched routes to fix islands-router issues (closes #4378) (#4380) --- router/src/flat_router.rs | 24 ++++++++++++++++-------- tachys/src/view/any_view.rs | 7 +++++++ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/router/src/flat_router.rs b/router/src/flat_router.rs index 8e426a34d7..6a2c374181 100644 --- a/router/src/flat_router.rs +++ b/router/src/flat_router.rs @@ -364,6 +364,12 @@ where #[derive(Debug)] pub(crate) struct MatchedRoute(pub String, pub AnyView); +impl MatchedRoute { + fn branch_name(&self) -> String { + format!("{:?}", self.1.as_type_id()) + } +} + impl Render for MatchedRoute { type State = <AnyView as Render>::State; @@ -414,8 +420,9 @@ impl RenderHtml for MatchedRoute { mark_branches: bool, extra_attrs: Vec<AnyAttribute>, ) { - if mark_branches && escape { - buf.open_branch(&self.0); + let branch_name = (mark_branches && escape).then(|| self.branch_name()); + if let Some(bn) = &branch_name { + buf.open_branch(bn); } self.1.to_html_with_buf( buf, @@ -424,8 +431,8 @@ impl RenderHtml for MatchedRoute { mark_branches, extra_attrs, ); - if mark_branches && escape { - buf.close_branch(&self.0); + if let Some(bn) = &branch_name { + buf.close_branch(bn); if *position == Position::NextChildAfterText { *position = Position::NextChild; } @@ -442,8 +449,9 @@ impl RenderHtml for MatchedRoute { ) where Self: Sized, { - if mark_branches && escape { - buf.open_branch(&self.0); + let branch_name = (mark_branches && escape).then(|| self.branch_name()); + if let Some(bn) = &branch_name { + buf.open_branch(bn); } self.1.to_html_async_with_buf::<OUT_OF_ORDER>( buf, @@ -452,8 +460,8 @@ impl RenderHtml for MatchedRoute { mark_branches, extra_attrs, ); - if mark_branches && escape { - buf.close_branch(&self.0); + if let Some(bn) = &branch_name { + buf.close_branch(bn); if *position == Position::NextChildAfterText { *position = Position::NextChild; } diff --git a/tachys/src/view/any_view.rs b/tachys/src/view/any_view.rs index dbf0eaf6a1..449937447e 100644 --- a/tachys/src/view/any_view.rs +++ b/tachys/src/view/any_view.rs @@ -79,6 +79,13 @@ pub struct AnyView { ) -> Pin<Box<dyn Future<Output = AnyViewState>>>, } +impl AnyView { + #[doc(hidden)] + pub fn as_type_id(&self) -> TypeId { + self.type_id + } +} + impl Debug for AnyView { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("AnyView") From b5845152b55e83001200fa9373a136fbf3521689 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Mon, 13 Oct 2025 10:52:45 -0400 Subject: [PATCH 59/91] fix: patching keyed store fields (closes #3957) --- reactive_stores/src/arc_field.rs | 30 +++++++++++ reactive_stores/src/deref.rs | 5 ++ reactive_stores/src/field.rs | 7 +++ reactive_stores/src/iter.rs | 4 ++ reactive_stores/src/keyed.rs | 18 ++++--- reactive_stores/src/lib.rs | 85 ++++++++++++++++++++++++------ reactive_stores/src/patch.rs | 2 +- reactive_stores/src/path.rs | 19 +++++++ reactive_stores/src/store_field.rs | 56 ++++++++++++++++++++ reactive_stores/src/subfield.rs | 4 ++ 10 files changed, 207 insertions(+), 23 deletions(-) diff --git a/reactive_stores/src/arc_field.rs b/reactive_stores/src/arc_field.rs index 15a6a5fa7c..90f4bd0e4d 100644 --- a/reactive_stores/src/arc_field.rs +++ b/reactive_stores/src/arc_field.rs @@ -30,6 +30,8 @@ where defined_at: &'static Location<'static>, path: Arc<dyn Fn() -> StorePath + Send + Sync>, get_trigger: Arc<dyn Fn(StorePath) -> StoreFieldTrigger + Send + Sync>, + get_trigger_unkeyed: + Arc<dyn Fn(StorePath) -> StoreFieldTrigger + Send + Sync>, read: Arc<dyn Fn() -> Option<StoreFieldReader<T>> + Send + Sync>, pub(crate) write: Arc<dyn Fn() -> Option<StoreFieldWriter<T>> + Send + Sync>, @@ -103,6 +105,10 @@ impl<T> StoreField for ArcField<T> { (self.get_trigger)(path) } + fn get_trigger_unkeyed(&self, path: StorePath) -> StoreFieldTrigger { + (self.get_trigger_unkeyed)(path) + } + fn path(&self) -> impl IntoIterator<Item = StorePathSegment> { (self.path)() } @@ -132,6 +138,9 @@ where defined_at: Location::caller(), path: Arc::new(move || value.path().into_iter().collect()), get_trigger: Arc::new(move |path| value.get_trigger(path)), + get_trigger_unkeyed: Arc::new(move |path| { + value.get_trigger_unkeyed(path) + }), read: Arc::new(move || value.reader().map(StoreFieldReader::new)), write: Arc::new(move || value.writer().map(StoreFieldWriter::new)), keys: Arc::new(move || value.keys()), @@ -158,6 +167,10 @@ where let value = value.clone(); move |path| value.get_trigger(path) }), + get_trigger_unkeyed: Arc::new({ + let value = value.clone(); + move |path| value.get_trigger_unkeyed(path) + }), read: Arc::new({ let value = value.clone(); move || value.reader().map(StoreFieldReader::new) @@ -202,6 +215,10 @@ where let value = value.clone(); move |path| value.get_trigger(path) }), + get_trigger_unkeyed: Arc::new({ + let value = value.clone(); + move |path| value.get_trigger_unkeyed(path) + }), read: Arc::new({ let value = value.clone(); move || value.reader().map(StoreFieldReader::new) @@ -245,6 +262,10 @@ where let value = value.clone(); move |path| value.get_trigger(path) }), + get_trigger_unkeyed: Arc::new({ + let value = value.clone(); + move |path| value.get_trigger_unkeyed(path) + }), read: Arc::new({ let value = value.clone(); move || value.reader().map(StoreFieldReader::new) @@ -289,6 +310,10 @@ where let value = value.clone(); move |path| value.get_trigger(path) }), + get_trigger_unkeyed: Arc::new({ + let value = value.clone(); + move |path| value.get_trigger_unkeyed(path) + }), read: Arc::new({ let value = value.clone(); move || value.reader().map(StoreFieldReader::new) @@ -337,6 +362,10 @@ where let value = value.clone(); move |path| value.get_trigger(path) }), + get_trigger_unkeyed: Arc::new({ + let value = value.clone(); + move |path| value.get_trigger_unkeyed(path) + }), read: Arc::new({ let value = value.clone(); move || value.reader().map(StoreFieldReader::new) @@ -368,6 +397,7 @@ impl<T> Clone for ArcField<T> { defined_at: self.defined_at, path: self.path.clone(), get_trigger: Arc::clone(&self.get_trigger), + get_trigger_unkeyed: Arc::clone(&self.get_trigger_unkeyed), read: Arc::clone(&self.read), write: Arc::clone(&self.write), keys: Arc::clone(&self.keys), diff --git a/reactive_stores/src/deref.rs b/reactive_stores/src/deref.rs index 6d3f58bcd1..93f6619ecf 100644 --- a/reactive_stores/src/deref.rs +++ b/reactive_stores/src/deref.rs @@ -68,6 +68,11 @@ where fn get_trigger(&self, path: StorePath) -> StoreFieldTrigger { self.inner.get_trigger(path) } + + fn get_trigger_unkeyed(&self, path: StorePath) -> StoreFieldTrigger { + self.inner.get_trigger_unkeyed(path) + } + fn path(&self) -> impl IntoIterator<Item = StorePathSegment> { self.inner.path() } diff --git a/reactive_stores/src/field.rs b/reactive_stores/src/field.rs index 63e6cc4e32..927fe31ec5 100644 --- a/reactive_stores/src/field.rs +++ b/reactive_stores/src/field.rs @@ -59,6 +59,13 @@ where .unwrap_or_default() } + fn get_trigger_unkeyed(&self, path: StorePath) -> StoreFieldTrigger { + self.inner + .try_get_value() + .map(|inner| inner.get_trigger_unkeyed(path)) + .unwrap_or_default() + } + fn path(&self) -> impl IntoIterator<Item = StorePathSegment> { self.inner .try_get_value() diff --git a/reactive_stores/src/iter.rs b/reactive_stores/src/iter.rs index 84aa7c0867..3c86bea1ee 100644 --- a/reactive_stores/src/iter.rs +++ b/reactive_stores/src/iter.rs @@ -84,6 +84,10 @@ where self.inner.get_trigger(path) } + fn get_trigger_unkeyed(&self, path: StorePath) -> StoreFieldTrigger { + self.inner.get_trigger_unkeyed(path) + } + fn reader(&self) -> Option<Self::Reader> { let inner = self.inner.reader()?; let index = self.index; diff --git a/reactive_stores/src/keyed.rs b/reactive_stores/src/keyed.rs index b7e6176b49..771226df78 100644 --- a/reactive_stores/src/keyed.rs +++ b/reactive_stores/src/keyed.rs @@ -110,6 +110,10 @@ where self.inner.get_trigger(path) } + fn get_trigger_unkeyed(&self, path: StorePath) -> StoreFieldTrigger { + self.inner.get_trigger_unkeyed(path) + } + fn reader(&self) -> Option<Self::Reader> { let inner = self.inner.reader()?; Some(Mapped::new_with_guard(inner, self.read)) @@ -432,7 +436,7 @@ where let this = keys .with_field_keys( inner.clone(), - |keys| keys.get(&self.key), + |keys| (keys.get(&self.key), vec![]), || self.inner.latest_keys(), ) .flatten() @@ -444,6 +448,10 @@ where self.inner.get_trigger(path) } + fn get_trigger_unkeyed(&self, path: StorePath) -> StoreFieldTrigger { + self.inner.get_trigger_unkeyed(path) + } + fn reader(&self) -> Option<Self::Reader> { let inner = self.inner.reader()?; @@ -452,7 +460,7 @@ where let index = keys .with_field_keys( inner_path, - |keys| keys.get(&self.key), + |keys| (keys.get(&self.key), vec![]), || self.inner.latest_keys(), ) .flatten() @@ -476,7 +484,7 @@ where let index = keys .with_field_keys( inner_path.clone(), - |keys| keys.get(&self.key), + |keys| (keys.get(&self.key), vec![]), || self.inner.latest_keys(), ) .flatten() @@ -624,9 +632,7 @@ where let latest = self.latest_keys(); keys.with_field_keys( inner_path, - |keys| { - keys.update(latest); - }, + |keys| ((), keys.update(latest)), || self.latest_keys(), ); } diff --git a/reactive_stores/src/lib.rs b/reactive_stores/src/lib.rs index d5fe77a99e..b19ac63549 100644 --- a/reactive_stores/src/lib.rs +++ b/reactive_stores/src/lib.rs @@ -364,13 +364,18 @@ where }) } - fn update(&mut self, iter: impl IntoIterator<Item = K>) { + fn update( + &mut self, + iter: impl IntoIterator<Item = K>, + ) -> Vec<(usize, StorePathSegment)> { let new_keys = iter .into_iter() .enumerate() .map(|(idx, key)| (key, idx)) .collect::<FxHashMap<K, usize>>(); + let mut index_keys = Vec::with_capacity(new_keys.len()); + // remove old keys and recycle the slots self.keys.retain(|key, old_entry| match new_keys.get(key) { Some(idx) => { @@ -385,14 +390,17 @@ where // add new keys for (key, idx) in new_keys { - // the suggestion doesn't compile because we need &mut for self.next_key(), - // and we don't want to call that until after the check - #[allow(clippy::map_entry)] - if !self.keys.contains_key(&key) { - let path = self.next_key(); - self.keys.insert(key, (path, idx)); + match self.keys.get(&key) { + Some((segment, idx)) => index_keys.push((*idx, *segment)), + None => { + let path = self.next_key(); + self.keys.insert(key, (path, idx)); + index_keys.push((idx, path)); + } } } + + index_keys } } @@ -415,14 +423,20 @@ type HashMap<K, V> = send_wrapper::SendWrapper< /// A map of the keys for a keyed subfield. #[derive(Clone)] -pub struct KeyMap(HashMap<StorePath, Box<dyn Any + Send + Sync>>); +pub struct KeyMap( + HashMap<StorePath, Box<dyn Any + Send + Sync>>, + HashMap<(StorePath, usize), StorePathSegment>, +); impl Default for KeyMap { fn default() -> Self { #[cfg(not(target_arch = "wasm32"))] - return Self(Default::default()); + return Self(Default::default(), Default::default()); #[cfg(target_arch = "wasm32")] - return Self(send_wrapper::SendWrapper::new(Default::default())); + return Self( + send_wrapper::SendWrapper::new(Default::default()), + send_wrapper::SendWrapper::new(Default::default()), + ); } } @@ -430,31 +444,70 @@ impl KeyMap { fn with_field_keys<K, T>( &self, path: StorePath, - fun: impl FnOnce(&mut FieldKeys<K>) -> T, + fun: impl FnOnce(&mut FieldKeys<K>) -> (T, Vec<(usize, StorePathSegment)>), initialize: impl FnOnce() -> Vec<K>, ) -> Option<T> where K: Debug + Hash + PartialEq + Eq + Send + Sync + 'static, { + let initial_keys = initialize(); + #[cfg(not(target_arch = "wasm32"))] let mut entry = self .0 - .entry(path) - .or_insert_with(|| Box::new(FieldKeys::new(initialize()))); + .entry(path.clone()) + .or_insert_with(|| Box::new(FieldKeys::new(initial_keys))); #[cfg(target_arch = "wasm32")] let entry = if !self.0.borrow().contains_key(&path) { - Some(Box::new(FieldKeys::new(initialize()))) + Some(Box::new(FieldKeys::new(initial_keys))) } else { None }; #[cfg(target_arch = "wasm32")] let mut map = self.0.borrow_mut(); #[cfg(target_arch = "wasm32")] - let entry = map.entry(path).or_insert_with(|| entry.unwrap()); + let entry = map.entry(path.clone()).or_insert_with(|| entry.unwrap()); let entry = entry.downcast_mut::<FieldKeys<K>>()?; - Some(fun(entry)) + let (result, new_keys) = fun(entry); + if !new_keys.is_empty() { + for (idx, segment) in new_keys { + #[cfg(not(target_arch = "wasm32"))] + self.1.insert((path.clone(), idx), segment); + + #[cfg(target_arch = "wasm32")] + self.1.borrow_mut().insert((path.clone(), idx), segment); + } + } + Some(result) + } + + fn contains_key(&self, key: &StorePath) -> bool { + #[cfg(not(target_arch = "wasm32"))] + { + self.0.contains_key(key) + } + + #[cfg(target_arch = "wasm32")] + { + self.0.borrow_mut().contains_key(key) + } + } + + fn get_key_for_index( + &self, + key: &(StorePath, usize), + ) -> Option<StorePathSegment> { + #[cfg(not(target_arch = "wasm32"))] + { + self.1.get(key).as_deref().copied() + } + + #[cfg(target_arch = "wasm32")] + { + self.1.borrow().get(key).as_deref().copied() + } } } diff --git a/reactive_stores/src/patch.rs b/reactive_stores/src/patch.rs index 136c0e4e64..6b8f788880 100644 --- a/reactive_stores/src/patch.rs +++ b/reactive_stores/src/patch.rs @@ -35,7 +35,7 @@ where // don't track the writer for the whole store writer.untrack(); let mut notify = |path: &StorePath| { - self.triggers_for_path(path.to_owned()).notify(); + self.triggers_for_path_unkeyed(path.to_owned()).notify(); }; writer.patch_field(new, &path, &mut notify); } diff --git a/reactive_stores/src/path.rs b/reactive_stores/src/path.rs index f225f6fbae..a88154b88a 100644 --- a/reactive_stores/src/path.rs +++ b/reactive_stores/src/path.rs @@ -11,6 +11,15 @@ impl IntoIterator for StorePath { } } +impl<'a> IntoIterator for &'a StorePath { + type Item = &'a StorePathSegment; + type IntoIter = std::slice::Iter<'a, StorePathSegment>; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter() + } +} + impl From<Vec<StorePathSegment>> for StorePath { fn from(value: Vec<StorePathSegment>) -> Self { Self(value) @@ -18,6 +27,16 @@ impl From<Vec<StorePathSegment>> for StorePath { } impl StorePath { + /// Creates a new path. + pub fn new() -> Self { + Self(Vec::new()) + } + + /// Creates a new path with storage capacity for `capacity` segments. + pub fn with_capacity(capacity: usize) -> Self { + Self(Vec::with_capacity(capacity)) + } + /// Adds a new segment to the path. pub fn push(&mut self, segment: impl Into<StorePathSegment>) { self.0.push(segment.into()); diff --git a/reactive_stores/src/store_field.rs b/reactive_stores/src/store_field.rs index 2e928327e7..7856d0788a 100644 --- a/reactive_stores/src/store_field.rs +++ b/reactive_stores/src/store_field.rs @@ -26,6 +26,14 @@ pub trait StoreField: Sized { #[track_caller] fn get_trigger(&self, path: StorePath) -> StoreFieldTrigger; + /// Returns the trigger that tracks access and updates for this field. + /// + /// This uses *unkeyed* paths: i.e., if any field in the path is keyed, it will + /// try to look up the key for the item at the index given in the path, rather than + /// the keyed item. + #[track_caller] + fn get_trigger_unkeyed(&self, path: StorePath) -> StoreFieldTrigger; + /// The path of this field (see [`StorePath`]). #[track_caller] fn path(&self) -> impl IntoIterator<Item = StorePathSegment>; @@ -84,6 +92,26 @@ pub trait StoreField: Sized { triggers } + + /// Returns triggers for the field at the given path, and all parent fields + fn triggers_for_path_unkeyed(&self, path: StorePath) -> Vec<ArcTrigger> { + // see notes on triggers_for_path() for additional comments on implementation + + let trigger = self.get_trigger_unkeyed(path.clone()); + let mut full_path = path; + + let mut triggers = Vec::with_capacity(full_path.len() + 2); + triggers.push(trigger.this.clone()); + triggers.push(trigger.children.clone()); + while !full_path.is_empty() { + full_path.pop(); + let inner = self.get_trigger_unkeyed(full_path.clone()); + triggers.push(inner.children.clone()); + } + triggers.reverse(); + + triggers + } } impl<T> StoreField for ArcStore<T> @@ -101,6 +129,26 @@ where trigger } + fn get_trigger_unkeyed(&self, path: StorePath) -> StoreFieldTrigger { + let orig_path = path.clone(); + + let mut path = StorePath::with_capacity(orig_path.len()); + for segment in &orig_path { + let parent_is_keyed = self.keys.contains_key(&path); + + if parent_is_keyed { + let key = self + .keys + .get_key_for_index(&(path.clone(), segment.0)) + .expect("could not find key for index"); + path.push(key); + } else { + path.push(*segment); + } + } + self.get_trigger(path) + } + #[track_caller] fn path(&self) -> impl IntoIterator<Item = StorePathSegment> { iter::empty() @@ -141,6 +189,14 @@ where .unwrap_or_default() } + #[track_caller] + fn get_trigger_unkeyed(&self, path: StorePath) -> StoreFieldTrigger { + self.inner + .try_get_value() + .map(|n| n.get_trigger_unkeyed(path)) + .unwrap_or_default() + } + #[track_caller] fn path(&self) -> impl IntoIterator<Item = StorePathSegment> { self.inner diff --git a/reactive_stores/src/subfield.rs b/reactive_stores/src/subfield.rs index 7dea75fa2e..823d3e2122 100644 --- a/reactive_stores/src/subfield.rs +++ b/reactive_stores/src/subfield.rs @@ -88,6 +88,10 @@ where self.inner.get_trigger(path) } + fn get_trigger_unkeyed(&self, path: StorePath) -> StoreFieldTrigger { + self.inner.get_trigger_unkeyed(path) + } + fn reader(&self) -> Option<Self::Reader> { let inner = self.inner.reader()?; Some(Mapped::new_with_guard(inner, self.read)) From cf8b0f2dcc0f03900717c412b7fb062c8c188720 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Mon, 13 Oct 2025 11:05:40 -0400 Subject: [PATCH 60/91] chore: bump `reactive_stores` minor version number --- Cargo.toml | 2 +- reactive_stores/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index aeea28e3f7..382869f496 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,7 +64,7 @@ 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.8" } -reactive_stores = { path = "./reactive_stores", version = "0.2.5" } +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.7" } server_fn_macro = { path = "./server_fn_macro", version = "0.8.7" } diff --git a/reactive_stores/Cargo.toml b/reactive_stores/Cargo.toml index 41e1d943b2..fcf9d2fc0d 100644 --- a/reactive_stores/Cargo.toml +++ b/reactive_stores/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "reactive_stores" -version = "0.2.5" +version = "0.3.0" authors = ["Greg Johnston"] license = "MIT" readme = "../README.md" From d24a7ae987670874f6e12efd7ef7dbe1529d8074 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 15:40:12 +0000 Subject: [PATCH 61/91] [autofix.ci] apply automated fixes --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index f85b2199af..11aa1d3d9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2898,7 +2898,7 @@ dependencies = [ [[package]] name = "reactive_stores" -version = "0.2.5" +version = "0.3.0" dependencies = [ "any_spawner", "dashmap", From 70dfa44995d2a2fb793433b68e84c66028867ff1 Mon Sep 17 00:00:00 2001 From: arpad voros <34720962+arpadav@users.noreply.github.com> Date: Fri, 17 Oct 2025 13:14:31 -0400 Subject: [PATCH 62/91] chore: propagate features from `leptos` to `server_fn` to avoid need to explicitly add dependency --- leptos/Cargo.toml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/leptos/Cargo.toml b/leptos/Cargo.toml index c630e9c473..0c2423d35e 100644 --- a/leptos/Cargo.toml +++ b/leptos/Cargo.toml @@ -88,6 +88,11 @@ ssr = [ ] nightly = ["leptos_macro/nightly", "reactive_graph/nightly", "tachys/nightly"] rkyv = ["server_fn/rkyv", "leptos_server/rkyv"] +serde-lite = ["server_fn/serde-lite", "leptos_server/serde-lite"] +cbor = ["server_fn/cbor"] +msgpack = ["server_fn/msgpack"] +postcard = ["server_fn/postcard"] +multipart = ["server_fn/multipart"] tracing = [ "dep:tracing", "reactive_graph/tracing", @@ -147,6 +152,11 @@ denylist = [ "trace-component-props", "spin", "islands", + "serde-lite", + "cbor", + "msgpack", + "postcard", + "multipart", ] skip_feature_sets = [ ["csr", "ssr"], From 3a715b676e8f785931b5cb2718cb783c5589a880 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Mon, 13 Oct 2025 17:31:45 -0400 Subject: [PATCH 63/91] fix: correctly track ancestors for `AtIndex` (closes #4385) --- reactive_stores/src/iter.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/reactive_stores/src/iter.rs b/reactive_stores/src/iter.rs index 3c86bea1ee..9db7c1c678 100644 --- a/reactive_stores/src/iter.rs +++ b/reactive_stores/src/iter.rs @@ -113,6 +113,23 @@ where fn keys(&self) -> Option<KeyMap> { self.inner.keys() } + + fn track_field(&self) { + let mut full_path = self.path().into_iter().collect::<StorePath>(); + let trigger = self.get_trigger(self.path().into_iter().collect()); + trigger.this.track(); + trigger.children.track(); + + // tracks `this` for all ancestors: i.e., it will track any change that is made + // directly to one of its ancestors, but not a change made to a *child* of an ancestor + // (which would end up with every subfield tracking its own siblings, because they are + // children of its parent) + while !full_path.is_empty() { + full_path.pop(); + let inner = self.get_trigger(full_path.clone()); + inner.this.track(); + } + } } impl<Inner, Prev> DefinedAt for AtIndex<Inner, Prev> From 87470e639aae77491fb8d7001587f403cac3c00c Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Fri, 17 Oct 2025 11:42:32 -0400 Subject: [PATCH 64/91] chore: add regression test for #3523 --- reactive_stores/src/lib.rs | 111 +++++++++++++++++++++++++++++-------- 1 file changed, 87 insertions(+), 24 deletions(-) diff --git a/reactive_stores/src/lib.rs b/reactive_stores/src/lib.rs index b19ac63549..5fa418ae15 100644 --- a/reactive_stores/src/lib.rs +++ b/reactive_stores/src/lib.rs @@ -885,6 +885,30 @@ mod tests { } } + #[derive(Debug, Clone, Store, Patch, Default)] + struct Foo { + id: i32, + bar: Bar, + } + + #[derive(Debug, Clone, Store, Patch, Default)] + struct Bar { + bar_signature: i32, + baz: Baz, + } + + #[derive(Debug, Clone, Store, Patch, Default)] + struct Baz { + more_data: i32, + baw: Baw, + } + + #[derive(Debug, Clone, Store, Patch, Default)] + struct Baw { + more_data: i32, + end: i32, + } + #[tokio::test] async fn mutating_field_triggers_effect() { _ = any_spawner::Executor::init_tokio(); @@ -1165,30 +1189,6 @@ mod tests { _ = any_spawner::Executor::init_tokio(); - #[derive(Debug, Clone, Store, Patch, Default)] - struct Foo { - id: i32, - bar: Bar, - } - - #[derive(Debug, Clone, Store, Patch, Default)] - struct Bar { - bar_signature: i32, - baz: Baz, - } - - #[derive(Debug, Clone, Store, Patch, Default)] - struct Baz { - more_data: i32, - baw: Baw, - } - - #[derive(Debug, Clone, Store, Patch, Default)] - struct Baw { - more_data: i32, - end: i32, - } - let store = Store::new(Foo { id: 42, bar: Bar { @@ -1272,4 +1272,67 @@ mod tests { assert_eq!(more_data_runs.get_value(), 3); assert_eq!(baz_baw_end_runs.get_value(), 3); } + + #[tokio::test] + async fn changing_parent_notifies_subfield() { + _ = any_spawner::Executor::init_tokio(); + + let combined_count = Arc::new(AtomicUsize::new(0)); + + let store = Store::new(Foo { + id: 42, + bar: Bar { + bar_signature: 69, + baz: Baz { + more_data: 9999, + baw: Baw { + more_data: 22, + end: 1112, + }, + }, + }, + }); + + let tracked_field = store.bar().baz().more_data(); + + Effect::new_sync({ + let combined_count = Arc::clone(&combined_count); + move |prev: Option<()>| { + if prev.is_none() { + println!("first run"); + } else { + println!("next run"); + } + + // we only track `more`, but this should still be notified + // when its parent fields `bar` or `baz` change + println!("{:?}", *tracked_field.read()); + combined_count.fetch_add(1, Ordering::Relaxed); + } + }); + tick().await; + tick().await; + + store.bar().baz().set(Baz { + more_data: 42, + baw: Baw { + more_data: 11, + end: 31, + }, + }); + tick().await; + store.bar().set(Bar { + bar_signature: 23, + baz: Baz { + more_data: 32, + baw: Baw { + more_data: 432, + end: 423, + }, + }, + }); + tick().await; + + assert_eq!(combined_count.load(Ordering::Relaxed), 3); + } } From 532311a17ec72a0bc8ff9d3f1043c100eea1b693 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Fri, 17 Oct 2025 11:42:43 -0400 Subject: [PATCH 65/91] chore: add regression test for #4385 --- reactive_stores/src/lib.rs | 40 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/reactive_stores/src/lib.rs b/reactive_stores/src/lib.rs index 5fa418ae15..9c0d8b8241 100644 --- a/reactive_stores/src/lib.rs +++ b/reactive_stores/src/lib.rs @@ -1335,4 +1335,44 @@ mod tests { assert_eq!(combined_count.load(Ordering::Relaxed), 3); } + + #[tokio::test] + async fn changing_parent_notifies_unkeyed_child() { + _ = any_spawner::Executor::init_tokio(); + + let combined_count = Arc::new(AtomicUsize::new(0)); + + let store = Store::new(data()); + + let tracked_field = store.todos().at_unkeyed(0); + + Effect::new_sync({ + let combined_count = Arc::clone(&combined_count); + move |prev: Option<()>| { + if prev.is_none() { + println!("first run"); + } else { + println!("next run"); + } + + // we only track `more`, but this should still be notified + // when its parent fields `bar` or `baz` change + println!("{:?}", *tracked_field.read()); + combined_count.fetch_add(1, Ordering::Relaxed); + } + }); + tick().await; + tick().await; + + store.todos().write().pop(); + tick().await; + + store.todos().write().push(Todo { + label: "another one".into(), + completed: false, + }); + tick().await; + + assert_eq!(combined_count.load(Ordering::Relaxed), 3); + } } From e2da9a1d89c37c483d3793c5d6c4a34b19c8f603 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Thu, 23 Oct 2025 12:50:48 -0400 Subject: [PATCH 66/91] leptos_actix-v0.8.6 (#4396) * leptos_actix-v0.8.6 * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- Cargo.lock | 2 +- integrations/actix/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 11aa1d3d9d..3012a37745 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1937,7 +1937,7 @@ dependencies = [ [[package]] name = "leptos_actix" -version = "0.8.5" +version = "0.8.6" dependencies = [ "actix-files", "actix-http", diff --git a/integrations/actix/Cargo.toml b/integrations/actix/Cargo.toml index 314f0e6e80..bb6b2908c4 100644 --- a/integrations/actix/Cargo.toml +++ b/integrations/actix/Cargo.toml @@ -4,7 +4,7 @@ authors = ["Greg Johnston"] license = "MIT" repository = "https://github.com/leptos-rs/leptos" description = "Actix integrations for the Leptos web framework." -version = "0.8.5" +version = "0.8.6" rust-version.workspace = true edition.workspace = true From 52eadaf549078e7e8f1baa4a8084819184b78c12 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Thu, 23 Oct 2025 14:00:29 -0400 Subject: [PATCH 67/91] fix: adding missing `dry_resolve()` call on `Suspend` (closes #4402) (#4404) --- tachys/src/reactive_graph/suspense.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tachys/src/reactive_graph/suspense.rs b/tachys/src/reactive_graph/suspense.rs index 6a3750b941..ccabd2b95e 100644 --- a/tachys/src/reactive_graph/suspense.rs +++ b/tachys/src/reactive_graph/suspense.rs @@ -468,7 +468,8 @@ where // // in this case, though, we can simply... discover that the data are already here, and then // stuff them back into a new Future, which can safely be polled after its completion - if let Some(inner) = self.inner.as_mut().now_or_never() { + if let Some(mut inner) = self.inner.as_mut().now_or_never() { + inner.dry_resolve(); self.inner = Box::pin(async move { inner }) as Pin<Box<dyn Future<Output = T> + Send>>; } From 66b99e790fe6e572b7edd065a6d1b66b771736f0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Oct 2025 12:41:11 -0400 Subject: [PATCH 68/91] chore(deps): bump actions/setup-node from 5 to 6 (#4398) Bumps [actions/setup-node](https://github.com/actions/setup-node) from 5 to 6. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/setup-node dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/run-cargo-make-task.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-cargo-make-task.yml b/.github/workflows/run-cargo-make-task.yml index 473dee7e4f..f6b8cb2688 100644 --- a/.github/workflows/run-cargo-make-task.yml +++ b/.github/workflows/run-cargo-make-task.yml @@ -88,7 +88,7 @@ jobs: run: trunk --version - name: Install Node.js if: contains(inputs.directory, 'examples') - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: 20 - uses: pnpm/action-setup@v4 From b6c78ec4af1748948fb55f7bb6f410848c5776e3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Oct 2025 12:41:27 -0400 Subject: [PATCH 69/91] chore(deps): bump playwright (#4399) Bumps the npm_and_yarn group with 1 update in the /projects/hexagonal-architecture/end2end directory: [playwright](https://github.com/microsoft/playwright). Updates `playwright` from 1.44.1 to 1.56.1 - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](https://github.com/microsoft/playwright/compare/v1.44.1...v1.56.1) --- updated-dependencies: - dependency-name: playwright dependency-version: 1.56.1 dependency-type: indirect dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../end2end/package-lock.json | 54 +++++++++---------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/projects/hexagonal-architecture/end2end/package-lock.json b/projects/hexagonal-architecture/end2end/package-lock.json index 260e8eb1fc..71158bc4a7 100644 --- a/projects/hexagonal-architecture/end2end/package-lock.json +++ b/projects/hexagonal-architecture/end2end/package-lock.json @@ -15,19 +15,18 @@ } }, "node_modules/@playwright/test": { - "version": "1.44.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz", - "integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==", + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", + "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", "dev": true, - "license": "Apache-2.0", "dependencies": { - "playwright": "1.44.1" + "playwright": "1.56.1" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/@types/node": { @@ -46,7 +45,6 @@ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, "hasInstallScript": true, - "license": "MIT", "optional": true, "os": [ "darwin" @@ -56,35 +54,33 @@ } }, "node_modules/playwright": { - "version": "1.44.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz", - "integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==", + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", + "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", "dev": true, - "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.44.1" + "playwright-core": "1.56.1" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=18" }, "optionalDependencies": { "fsevents": "2.3.2" } }, "node_modules/playwright-core": { - "version": "1.44.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz", - "integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==", + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", + "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", "dev": true, - "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/typescript": { @@ -111,12 +107,12 @@ }, "dependencies": { "@playwright/test": { - "version": "1.44.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz", - "integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==", + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", + "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", "dev": true, "requires": { - "playwright": "1.44.1" + "playwright": "1.56.1" } }, "@types/node": { @@ -136,19 +132,19 @@ "optional": true }, "playwright": { - "version": "1.44.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz", - "integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==", + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", + "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", "dev": true, "requires": { "fsevents": "2.3.2", - "playwright-core": "1.44.1" + "playwright-core": "1.56.1" } }, "playwright-core": { - "version": "1.44.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz", - "integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==", + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", + "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", "dev": true }, "typescript": { From 6d4591f6ed718cea8a0061e097335814b2ba0d67 Mon Sep 17 00:00:00 2001 From: arpad voros <34720962+arpadav@users.noreply.github.com> Date: Fri, 24 Oct 2025 12:42:01 -0400 Subject: [PATCH 70/91] feat: add `bitcode` encoding/decoding to server functions (#4376) --- Cargo.lock | 43 +++++++++++++++++++++++++++++ Cargo.toml | 1 + leptos/Cargo.toml | 3 +- server_fn/Cargo.toml | 2 ++ server_fn/src/codec/bitcode.rs | 49 +++++++++++++++++++++++++++++++++ server_fn/src/codec/mod.rs | 5 ++++ server_fn/src/lib.rs | 2 ++ server_fn/tests/server_macro.rs | 3 +- server_fn_macro/src/lib.rs | 8 ++++++ 9 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 server_fn/src/codec/bitcode.rs diff --git a/Cargo.lock b/Cargo.lock index 3012a37745..1a5072d3bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -283,6 +283,12 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "async-executor" version = "1.13.3" @@ -487,6 +493,30 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bitcode" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "648bd963d2e5d465377acecfb4b827f9f553b6bc97a8f61715779e9ed9e52b74" +dependencies = [ + "arrayvec", + "bitcode_derive", + "bytemuck", + "glam", + "serde", +] + +[[package]] +name = "bitcode_derive" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffebfc2d28a12b262c303cb3860ee77b91bd83b1f20f0bd2a9693008e2f55a9e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "bitflags" version = "2.9.4" @@ -552,6 +582,12 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "bytemuck" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" + [[package]] name = "byteorder" version = "1.5.0" @@ -1306,6 +1342,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "glam" +version = "0.30.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e12d847aeb25f41be4c0ec9587d624e9cd631bc007a8fd7ce3f5851e064c6460" + [[package]] name = "glib" version = "0.20.12" @@ -3429,6 +3471,7 @@ dependencies = [ "actix-ws", "axum", "base64", + "bitcode", "bytes", "ciborium", "const-str", diff --git a/Cargo.toml b/Cargo.toml index 382869f496..a7b6f24993 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -134,6 +134,7 @@ inventory = { default-features = false, version = "0.3.21" } config = { default-features = false, version = "0.15.14" } camino = { default-features = false, version = "1.2.1" } ciborium = { default-features = false, version = "0.2.2" } +bitcode = { default-features = false, version = "0.6.6" } multer = { default-features = false, version = "3.1.0" } leptos-spin-macro = { default-features = false, version = "0.2.0" } sledgehammer_utils = { default-features = false, version = "0.3.1" } diff --git a/leptos/Cargo.toml b/leptos/Cargo.toml index 0c2423d35e..d3e58ea0b1 100644 --- a/leptos/Cargo.toml +++ b/leptos/Cargo.toml @@ -88,6 +88,7 @@ ssr = [ ] nightly = ["leptos_macro/nightly", "reactive_graph/nightly", "tachys/nightly"] rkyv = ["server_fn/rkyv", "leptos_server/rkyv"] +bitcode = ["server_fn/bitcode"] serde-lite = ["server_fn/serde-lite", "leptos_server/serde-lite"] cbor = ["server_fn/cbor"] msgpack = ["server_fn/msgpack"] @@ -152,6 +153,7 @@ denylist = [ "trace-component-props", "spin", "islands", + "bitcode", "serde-lite", "cbor", "msgpack", @@ -163,7 +165,6 @@ skip_feature_sets = [ ["csr", "hydrate"], ["ssr", "hydrate"], ["serde", "serde-lite"], - ["serde-lite", "miniserde"], ["serde", "miniserde"], ["serde", "rkyv"], ["miniserde", "rkyv"], diff --git a/server_fn/Cargo.toml b/server_fn/Cargo.toml index 6f0bcfb4a0..dd86e45983 100644 --- a/server_fn/Cargo.toml +++ b/server_fn/Cargo.toml @@ -64,6 +64,7 @@ http-body-util = { optional = true, workspace = true, default-features = true } rkyv = { optional = true, workspace = true, default-features = true } rmp-serde = { optional = true, workspace = true, default-features = true } base64 = { workspace = true, default-features = true } +bitcode = { optional = true, workspace = true, default-features = true } # client gloo-net = { optional = true, workspace = true, default-features = true } @@ -126,6 +127,7 @@ cbor = ["dep:ciborium"] rkyv = ["dep:rkyv"] msgpack = ["dep:rmp-serde"] postcard = ["dep:postcard"] +bitcode = ["dep:bitcode"] default-tls = ["reqwest?/default-tls"] rustls = ["reqwest?/rustls-tls", "tokio-tungstenite?/rustls"] reqwest = ["dep:reqwest", "dep:tokio-tungstenite", "dep:tokio"] diff --git a/server_fn/src/codec/bitcode.rs b/server_fn/src/codec/bitcode.rs new file mode 100644 index 0000000000..d0a18982cb --- /dev/null +++ b/server_fn/src/codec/bitcode.rs @@ -0,0 +1,49 @@ +use super::{Patch, Post, Put}; +use crate::{ContentType, Decodes, Encodes, Format, FormatType}; +use bytes::Bytes; + +/// Serializes and deserializes with [`bitcode`]. +pub struct BitcodeEncoding; + +impl ContentType for BitcodeEncoding { + const CONTENT_TYPE: &'static str = "application/bitcode"; +} + +impl FormatType for BitcodeEncoding { + const FORMAT_TYPE: Format = Format::Binary; +} + +impl<T> Encodes<T> for BitcodeEncoding +where + T: bitcode::Encode, +{ + type Error = std::convert::Infallible; + + fn encode(value: &T) -> Result<Bytes, Self::Error> { + Ok(Bytes::from(bitcode::encode(value))) + } +} + +impl<T> Decodes<T> for BitcodeEncoding +where + T: bitcode::DecodeOwned, +{ + type Error = bitcode::Error; + + fn decode(bytes: Bytes) -> Result<T, Self::Error> { + bitcode::decode(bytes.as_ref()) + } +} + +/// Pass arguments and receive responses using `bitcode` in a `POST` request. +pub type Bitcode = Post<BitcodeEncoding>; + +/// Pass arguments and receive responses using `bitcode` in the body of a `PATCH` request. +/// **Note**: Browser support for `PATCH` requests without JS/WASM may be poor. +/// Consider using a `POST` request if functionality without JS/WASM is required. +pub type PatchBitcode = Patch<BitcodeEncoding>; + +/// Pass arguments and receive responses using `bitcode` in the body of a `PUT` request. +/// **Note**: Browser support for `PUT` requests without JS/WASM may be poor. +/// Consider using a `POST` request if functionality without JS/WASM is required. +pub type PutBitcode = Put<BitcodeEncoding>; diff --git a/server_fn/src/codec/mod.rs b/server_fn/src/codec/mod.rs index fc500e6ac2..99fcbe46fc 100644 --- a/server_fn/src/codec/mod.rs +++ b/server_fn/src/codec/mod.rs @@ -50,6 +50,11 @@ mod postcard; #[cfg(feature = "postcard")] pub use postcard::*; +#[cfg(feature = "bitcode")] +mod bitcode; +#[cfg(feature = "bitcode")] +pub use bitcode::*; + mod patch; pub use patch::*; mod post; diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index da47ffcfd0..3e52d1da91 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -133,6 +133,8 @@ pub use ::bytes as bytes_export; #[doc(hidden)] pub use ::http as http_export; use base64::{engine::general_purpose::STANDARD_NO_PAD, DecodeError, Engine}; +#[cfg(feature = "bitcode")] +pub use bitcode; // re-exported to make it possible to implement a custom Client without adding a separate // dependency on `bytes` pub use bytes::Bytes; diff --git a/server_fn/tests/server_macro.rs b/server_fn/tests/server_macro.rs index 8363dbbdd6..fabe643190 100644 --- a/server_fn/tests/server_macro.rs +++ b/server_fn/tests/server_macro.rs @@ -10,7 +10,8 @@ feature = "multipart", feature = "serde-lite", feature = "cbor", - feature = "msgpack" + feature = "msgpack", + feature = "bitcode", )) ))] diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index b37c414a9b..d5ec64576a 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -331,6 +331,7 @@ impl ServerFnCall { enum PathInfo { Serde, Rkyv, + Bitcode, None, } @@ -341,6 +342,12 @@ impl ServerFnCall { Clone, #server_fn_path::rkyv::Archive, #server_fn_path::rkyv::Serialize, #server_fn_path::rkyv::Deserialize }, ), + Some("Bitcode") => ( + PathInfo::Bitcode, + quote! { + Clone, #server_fn_path::bitcode::Encode, #server_fn_path::bitcode::Decode + }, + ), Some("MultipartFormData") | Some("Streaming") | Some("StreamingText") => (PathInfo::None, quote! {}), @@ -376,6 +383,7 @@ impl ServerFnCall { #[serde(crate = #serde_path)] } } + PathInfo::Bitcode => quote! {}, PathInfo::Rkyv => quote! {}, PathInfo::None => quote! {}, }; From bf40ed8ed6af558cdfb19dab88fcc7c04195bc72 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Fri, 24 Oct 2025 13:06:04 -0400 Subject: [PATCH 71/91] chore: publish patch versions --- Cargo.lock | 20 ++++++++++---------- Cargo.toml | 16 ++++++++-------- any_error/Cargo.toml | 2 +- leptos/Cargo.toml | 2 +- leptos_macro/Cargo.toml | 2 +- reactive_graph/Cargo.toml | 2 +- router/Cargo.toml | 2 +- server_fn/Cargo.toml | 2 +- server_fn_macro/Cargo.toml | 2 +- tachys/Cargo.toml | 2 +- 10 files changed, 26 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1a5072d3bb..45cb874a54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1920,7 +1920,7 @@ checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" [[package]] name = "leptos" -version = "0.8.10" +version = "0.8.11" dependencies = [ "any_spawner", "base64", @@ -2087,7 +2087,7 @@ dependencies = [ [[package]] name = "leptos_macro" -version = "0.8.9" +version = "0.8.10" dependencies = [ "attribute-derive", "cfg-if", @@ -2107,7 +2107,7 @@ dependencies = [ "rustc_version", "serde", "server_fn", - "server_fn_macro 0.8.7", + "server_fn_macro 0.8.8", "syn 2.0.106", "tracing", "trybuild", @@ -2131,7 +2131,7 @@ dependencies = [ [[package]] name = "leptos_router" -version = "0.8.8" +version = "0.8.9" dependencies = [ "any_spawner", "either_of", @@ -2915,7 +2915,7 @@ dependencies = [ [[package]] name = "reactive_graph" -version = "0.2.8" +version = "0.2.9" dependencies = [ "any_spawner", "async-lock", @@ -3465,7 +3465,7 @@ dependencies = [ [[package]] name = "server_fn" -version = "0.8.7" +version = "0.8.8" dependencies = [ "actix-web", "actix-ws", @@ -3529,7 +3529,7 @@ dependencies = [ [[package]] name = "server_fn_macro" -version = "0.8.7" +version = "0.8.8" dependencies = [ "const_format", "convert_case 0.8.0", @@ -3544,7 +3544,7 @@ dependencies = [ name = "server_fn_macro_default" version = "0.8.5" dependencies = [ - "server_fn_macro 0.8.7", + "server_fn_macro 0.8.8", "syn 2.0.106", ] @@ -3791,7 +3791,7 @@ dependencies = [ [[package]] name = "tachys" -version = "0.2.9" +version = "0.2.10" dependencies = [ "any_spawner", "async-trait", @@ -3914,7 +3914,7 @@ dependencies = [ [[package]] name = "throw_error" -version = "0.3.0" +version = "0.3.1" dependencies = [ "anyhow", "pin-project-lite", diff --git a/Cargo.toml b/Cargo.toml index a7b6f24993..798020dcc6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,31 +45,31 @@ rust-version = "1.88" [workspace.dependencies] # members -throw_error = { path = "./any_error/", version = "0.3.0" } +throw_error = { path = "./any_error/", version = "0.3.1" } 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.10" } +leptos = { path = "./leptos", version = "0.8.11" } leptos_config = { path = "./leptos_config", version = "0.8.7" } 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.9" } -leptos_router = { path = "./router", version = "0.8.8" } +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_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.8" } +reactive_graph = { path = "./reactive_graph", version = "0.2.9" } 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.7" } -server_fn_macro = { path = "./server_fn_macro", version = "0.8.7" } +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.9" } +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" } diff --git a/any_error/Cargo.toml b/any_error/Cargo.toml index 2650b3b394..f136713dab 100644 --- a/any_error/Cargo.toml +++ b/any_error/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "throw_error" -version = "0.3.0" +version = "0.3.1" authors = ["Greg Johnston"] license = "MIT" readme = "../README.md" diff --git a/leptos/Cargo.toml b/leptos/Cargo.toml index d3e58ea0b1..a2ef3704e8 100644 --- a/leptos/Cargo.toml +++ b/leptos/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "leptos" -version = "0.8.10" +version = "0.8.11" authors = ["Greg Johnston"] license = "MIT" repository = "https://github.com/leptos-rs/leptos" diff --git a/leptos_macro/Cargo.toml b/leptos_macro/Cargo.toml index 35449becd1..63b5130184 100644 --- a/leptos_macro/Cargo.toml +++ b/leptos_macro/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "leptos_macro" -version = "0.8.9" +version = "0.8.10" authors = ["Greg Johnston"] license = "MIT" repository = "https://github.com/leptos-rs/leptos" diff --git a/reactive_graph/Cargo.toml b/reactive_graph/Cargo.toml index a073966e5c..acd9a42522 100644 --- a/reactive_graph/Cargo.toml +++ b/reactive_graph/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "reactive_graph" -version = "0.2.8" +version = "0.2.9" authors = ["Greg Johnston"] license = "MIT" readme = "../README.md" diff --git a/router/Cargo.toml b/router/Cargo.toml index 877a5f1ad9..e808747e5b 100644 --- a/router/Cargo.toml +++ b/router/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "leptos_router" -version = "0.8.8" +version = "0.8.9" authors = ["Greg Johnston", "Ben Wishovich"] license = "MIT" readme = "../README.md" diff --git a/server_fn/Cargo.toml b/server_fn/Cargo.toml index dd86e45983..35c2fc9b44 100644 --- a/server_fn/Cargo.toml +++ b/server_fn/Cargo.toml @@ -5,7 +5,7 @@ license = "MIT" repository = "https://github.com/leptos-rs/leptos" description = "RPC for any web framework." readme = "../README.md" -version = "0.8.7" +version = "0.8.8" rust-version.workspace = true edition.workspace = true diff --git a/server_fn_macro/Cargo.toml b/server_fn_macro/Cargo.toml index f2725e0fc6..4d8afd2433 100644 --- a/server_fn_macro/Cargo.toml +++ b/server_fn_macro/Cargo.toml @@ -5,7 +5,7 @@ license = "MIT" repository = "https://github.com/leptos-rs/leptos" description = "RPC for any web framework." readme = "../README.md" -version = "0.8.7" +version = "0.8.8" edition.workspace = true [dependencies] diff --git a/tachys/Cargo.toml b/tachys/Cargo.toml index bcab009f3c..aa75106e38 100644 --- a/tachys/Cargo.toml +++ b/tachys/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tachys" -version = "0.2.9" +version = "0.2.10" authors = ["Greg Johnston"] license = "MIT" readme = "../README.md" From 1bf1e139c2694840de8ca2e8c69fb97cfaed1f32 Mon Sep 17 00:00:00 2001 From: WorldSEnder <WorldSEnder@users.noreply.github.com> Date: Sat, 25 Oct 2025 03:13:55 +0200 Subject: [PATCH 72/91] feat: replace vendored wasm-split with out-of-repository version (#4369) --- Cargo.lock | 17 ++-- Cargo.toml | 3 +- leptos/Cargo.toml | 2 +- leptos/src/lib.rs | 3 +- leptos_macro/src/lazy.rs | 49 ++++++--- router_macro/src/lib.rs | 9 +- wasm_split/Cargo.toml | 17 ---- wasm_split/Makefile.toml | 1 - wasm_split/README.md | 9 -- wasm_split/src/lib.rs | 107 -------------------- wasm_split_macros/Cargo.toml | 21 ---- wasm_split_macros/Makefile.toml | 1 - wasm_split_macros/README.md | 9 -- wasm_split_macros/src/lib.rs | 172 -------------------------------- 14 files changed, 51 insertions(+), 369 deletions(-) delete mode 100644 wasm_split/Cargo.toml delete mode 100644 wasm_split/Makefile.toml delete mode 100644 wasm_split/README.md delete mode 100644 wasm_split/src/lib.rs delete mode 100644 wasm_split_macros/Cargo.toml delete mode 100644 wasm_split_macros/Makefile.toml delete mode 100644 wasm_split_macros/README.md delete mode 100644 wasm_split_macros/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 45cb874a54..fa015c8534 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1695,7 +1695,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.0", "tokio", "tower-service", "tracing", @@ -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", ] @@ -4602,23 +4602,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]] diff --git a/Cargo.toml b/Cargo.toml index 798020dcc6..0da0001c1a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,8 +70,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" } # members deps async-once-cell = { default-features = false, version = "0.5.3" } @@ -174,6 +172,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 diff --git a/leptos/Cargo.toml b/leptos/Cargo.toml index a2ef3704e8..5c5495852a 100644 --- a/leptos/Cargo.toml +++ b/leptos/Cargo.toml @@ -57,7 +57,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 } diff --git a/leptos/src/lib.rs b/leptos/src/lib.rs index c97d185d30..99af27c630 100644 --- a/leptos/src/lib.rs +++ b/leptos/src/lib.rs @@ -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; diff --git a/leptos_macro/src/lazy.rs b/leptos_macro/src/lazy.rs index 9112697af4..4128634b6e 100644 --- a/leptos_macro/src/lazy.rs +++ b/leptos_macro/src/lazy.rs @@ -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 } } diff --git a/router_macro/src/lib.rs b/router_macro/src/lib.rs index 60d60dac3f..8e04832c66 100644 --- a/router_macro/src/lib.rs +++ b/router_macro/src/lib.rs @@ -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(), diff --git a/wasm_split/Cargo.toml b/wasm_split/Cargo.toml deleted file mode 100644 index 9a2ab9d669..0000000000 --- a/wasm_split/Cargo.toml +++ /dev/null @@ -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 diff --git a/wasm_split/Makefile.toml b/wasm_split/Makefile.toml deleted file mode 100644 index 3d822c68da..0000000000 --- a/wasm_split/Makefile.toml +++ /dev/null @@ -1 +0,0 @@ -extend = { path = "../cargo-make/main.toml" } diff --git a/wasm_split/README.md b/wasm_split/README.md deleted file mode 100644 index 6f065cf858..0000000000 --- a/wasm_split/README.md +++ /dev/null @@ -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. diff --git a/wasm_split/src/lib.rs b/wasm_split/src/lib.rs deleted file mode 100644 index 6104ad4815..0000000000 --- a/wasm_split/src/lib.rs +++ /dev/null @@ -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); -} diff --git a/wasm_split_macros/Cargo.toml b/wasm_split_macros/Cargo.toml deleted file mode 100644 index 4c9c4de14f..0000000000 --- a/wasm_split_macros/Cargo.toml +++ /dev/null @@ -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 diff --git a/wasm_split_macros/Makefile.toml b/wasm_split_macros/Makefile.toml deleted file mode 100644 index 3d822c68da..0000000000 --- a/wasm_split_macros/Makefile.toml +++ /dev/null @@ -1 +0,0 @@ -extend = { path = "../cargo-make/main.toml" } diff --git a/wasm_split_macros/README.md b/wasm_split_macros/README.md deleted file mode 100644 index 2ea64ef290..0000000000 --- a/wasm_split_macros/README.md +++ /dev/null @@ -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. diff --git a/wasm_split_macros/src/lib.rs b/wasm_split_macros/src/lib.rs deleted file mode 100644 index 0ccc26dcf5..0000000000 --- a/wasm_split_macros/src/lib.rs +++ /dev/null @@ -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() -} From 45aa5644e9b208e5390b4155970476a490c214b1 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Mon, 27 Oct 2025 20:05:04 -0400 Subject: [PATCH 73/91] chore: publish patch versions --- Cargo.lock | 6 +++--- Cargo.toml | 6 +++--- leptos/Cargo.toml | 2 +- leptos_macro/Cargo.toml | 2 +- router_macro/Cargo.toml | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fa015c8534..24ebe402bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1920,7 +1920,7 @@ checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" [[package]] name = "leptos" -version = "0.8.11" +version = "0.8.12" dependencies = [ "any_spawner", "base64", @@ -2087,7 +2087,7 @@ dependencies = [ [[package]] name = "leptos_macro" -version = "0.8.10" +version = "0.8.11" dependencies = [ "attribute-derive", "cfg-if", @@ -2155,7 +2155,7 @@ dependencies = [ [[package]] name = "leptos_router_macro" -version = "0.8.5" +version = "0.8.6" dependencies = [ "leptos", "leptos_macro", diff --git a/Cargo.toml b/Cargo.toml index 0da0001c1a..e059b96af7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,14 +50,14 @@ 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 = { path = "./leptos", version = "0.8.12" } leptos_config = { path = "./leptos_config", version = "0.8.7" } 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_macro = { path = "./leptos_macro", version = "0.8.11" } leptos_router = { path = "./router", version = "0.8.9" } -leptos_router_macro = { path = "./router_macro", version = "0.8.5" } +leptos_router_macro = { path = "./router_macro", version = "0.8.6" } leptos_server = { path = "./leptos_server", version = "0.8.5" } leptos_meta = { path = "./meta", version = "0.8.5" } next_tuple = { path = "./next_tuple", version = "0.1.0" } diff --git a/leptos/Cargo.toml b/leptos/Cargo.toml index 5c5495852a..dd39f7c228 100644 --- a/leptos/Cargo.toml +++ b/leptos/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "leptos" -version = "0.8.11" +version = "0.8.12" authors = ["Greg Johnston"] license = "MIT" repository = "https://github.com/leptos-rs/leptos" diff --git a/leptos_macro/Cargo.toml b/leptos_macro/Cargo.toml index 63b5130184..eeb3014506 100644 --- a/leptos_macro/Cargo.toml +++ b/leptos_macro/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "leptos_macro" -version = "0.8.10" +version = "0.8.11" authors = ["Greg Johnston"] license = "MIT" repository = "https://github.com/leptos-rs/leptos" diff --git a/router_macro/Cargo.toml b/router_macro/Cargo.toml index f7f988581e..7272fa54af 100644 --- a/router_macro/Cargo.toml +++ b/router_macro/Cargo.toml @@ -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" From a76741d1915e10ed43678ac275e14122459beb81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= <tswast@gmail.com> Date: Wed, 29 Oct 2025 07:35:13 -0500 Subject: [PATCH 74/91] chore: add homepage to leptos cargo metadata (#4417) This updates the links at Are We Web Yet. --- leptos/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/leptos/Cargo.toml b/leptos/Cargo.toml index dd39f7c228..b5a3cc4a0c 100644 --- a/leptos/Cargo.toml +++ b/leptos/Cargo.toml @@ -4,6 +4,7 @@ version = "0.8.12" 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 From 791a797c4780ceec8cab78a1ba5b2e0843f461fb Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Wed, 29 Oct 2025 21:07:52 -0400 Subject: [PATCH 75/91] examples: clarify behavior of upload-with-progress demo (closes #4397) (#4420) --- examples/server_fns_axum/src/app.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/examples/server_fns_axum/src/app.rs b/examples/server_fns_axum/src/app.rs index f81a38921b..cb302f2a14 100644 --- a/examples/server_fns_axum/src/app.rs +++ b/examples/server_fns_axum/src/app.rs @@ -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; From 83ecdc89bddee7165b6a2139ce02f1eff8bfccd4 Mon Sep 17 00:00:00 2001 From: Zak Stucke <zakstucke@gmail.com> Date: Sun, 2 Nov 2025 15:24:26 +0200 Subject: [PATCH 76/91] Force cleanup even if there are other references to the root owner --- integrations/utils/src/lib.rs | 2 +- reactive_graph/src/owner.rs | 19 +++++++++++++++++++ router/src/static_routes.rs | 2 +- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/integrations/utils/src/lib.rs b/integrations/utils/src/lib.rs index b4438eed98..880bacb4c7 100644 --- a/integrations/utils/src/lib.rs +++ b/integrations/utils/src/lib.rs @@ -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() })), )); diff --git a/reactive_graph/src/owner.rs b/reactive_graph/src/owner.rs index 8ad9a7d669..070af695ea 100644 --- a/reactive_graph/src/owner.rs +++ b/reactive_graph/src/owner.rs @@ -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( diff --git a/router/src/static_routes.rs b/router/src/static_routes.rs index ab6c602042..9d5bb51ece 100644 --- a/router/src/static_routes.rs +++ b/router/src/static_routes.rs @@ -369,7 +369,7 @@ impl ResolvedStaticPath { eprintln!("{e}"); } } - owner.unset(); + owner.unset_with_forced_cleanup(); } } }); From bf76c64dff9c28c3acd0dacdfd550cd7cbb1c231 Mon Sep 17 00:00:00 2001 From: Zak Stucke <zakstucke@gmail.com> Date: Sun, 2 Nov 2025 16:16:03 +0200 Subject: [PATCH 77/91] Clippy --- leptos_config/src/lib.rs | 22 ++++++++-------------- tachys/src/view/keyed.rs | 9 ++------- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/leptos_config/src/lib.rs b/leptos_config/src/lib.rs index f4d560ec50..97b45b0ebc 100644 --- a/leptos_config/src/lib.rs +++ b/leptos_config/src/lib.rs @@ -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() { diff --git a/tachys/src/view/keyed.rs b/tachys/src/view/keyed.rs index 2278bb2810..acd499c25c 100644 --- a/tachys/src/view/keyed.rs +++ b/tachys/src/view/keyed.rs @@ -642,18 +642,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, From bece288bfa907b1be6db6fa78e41ac5259c182d5 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Sun, 2 Nov 2025 14:49:51 -0500 Subject: [PATCH 78/91] fix: check custom element tag name when rebuilding (#4413) --- tachys/src/html/element/mod.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tachys/src/html/element/mod.rs b/tachys/src/html/element/mod.rs index 03ad87bb6c..9f28146a70 100644 --- a/tachys/src/html/element/mod.rs +++ b/tachys/src/html/element/mod.rs @@ -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; From 8c0092cb813090ebec7c19faffcd9c3c35f128b8 Mon Sep 17 00:00:00 2001 From: Alexis Fontaine <alexisfontaine@users.noreply.github.com> Date: Tue, 4 Nov 2025 13:42:38 +0000 Subject: [PATCH 79/91] chore: relax `Debug` trait bound on tuples `PossibleRouteMatch` implementation (#4428) --- router/src/matching/horizontal/tuples.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/router/src/matching/horizontal/tuples.rs b/router/src/matching/horizontal/tuples.rs index 0b17135f66..ea3971e872 100644 --- a/router/src/matching/horizontal/tuples.rs +++ b/router/src/matching/horizontal/tuples.rs @@ -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),*, { From 4d1c2fefdd7bb90e0991e5ad96e9461e29b10010 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Fri, 7 Nov 2025 13:43:18 -0500 Subject: [PATCH 80/91] fix: remove possibility of SendWrapper errors on server by using conditional compilation instead of overloading `.dry_resolve()` (closes #4432, #4402) (#4433) --- tachys/src/html/directive.rs | 20 ++++++++------------ tachys/src/html/event.rs | 10 ++-------- tachys/src/html/property.rs | 10 ++-------- 3 files changed, 12 insertions(+), 28 deletions(-) diff --git a/tachys/src/html/directive.rs b/tachys/src/html/directive.rs index b38af28fc3..5fab0b26fe 100644 --- a/tachys/src/html/directive.rs +++ b/tachys/src/html/directive.rs @@ -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 diff --git a/tachys/src/html/event.rs b/tachys/src/html/event.rs index 8083b5e9f7..22b3df3122 100644 --- a/tachys/src/html/event.rs +++ b/tachys/src/html/event.rs @@ -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 diff --git a/tachys/src/html/property.rs b/tachys/src/html/property.rs index ac94a1808e..9c435b82c7 100644 --- a/tachys/src/html/property.rs +++ b/tachys/src/html/property.rs @@ -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 From 4e2d6a0a0868e74f1798875b731fe8f0133f25a7 Mon Sep 17 00:00:00 2001 From: Marco Kuoni <mail@marcokuoni.ch> Date: Mon, 10 Nov 2025 02:23:40 +0100 Subject: [PATCH 81/91] docs: add `--split` to command for `lazy_routes` example (#4440) --- examples/lazy_routes/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/lazy_routes/README.md b/examples/lazy_routes/README.md index 834497bdc0..7f4e5288a0 100644 --- a/examples/lazy_routes/README.md +++ b/examples/lazy_routes/README.md @@ -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. From c65f5761db729ebff1170563ed9c75ecb962587c Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Tue, 11 Nov 2025 15:51:21 -0500 Subject: [PATCH 82/91] Update README.md --- examples/islands_router/README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/examples/islands_router/README.md b/examples/islands_router/README.md index b6f6b25c6f..30dcc8314e 100644 --- a/examples/islands_router/README.md +++ b/examples/islands_router/README.md @@ -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`. From ceb7195b85a6a96d1ab0e3d0a847e694f6f15915 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Mon, 17 Nov 2025 21:00:41 -0500 Subject: [PATCH 83/91] chore: clean up up warning behavior for resources that depend on other resources (#4415) (closes #3372) --- leptos_server/src/resource.rs | 43 +++++++++++++++++++++++++++-- reactive_graph/src/effect/effect.rs | 26 +++++++++++------ 2 files changed, 58 insertions(+), 11 deletions(-) diff --git a/leptos_server/src/resource.rs b/leptos_server/src/resource.rs index 91289c3194..5065931bae 100644 --- a/leptos_server/src/resource.rs +++ b/leptos_server/src/resource.rs @@ -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!( diff --git a/reactive_graph/src/effect/effect.rs b/reactive_graph/src/effect/effect.rs index 60da480b9e..f05f34b573 100644 --- a/reactive_graph/src/effect/effect.rs +++ b/reactive_graph/src/effect/effect.rs @@ -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> From eeda4d19294170cc6aab0465d106a8bacdcb1b99 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Mon, 17 Nov 2025 21:36:56 -0500 Subject: [PATCH 84/91] fix: track resources in Suspense that are read conditionally behind other resource reads (see #4430) (#4444) --- leptos/src/suspense_component.rs | 77 ++++++++++++++++++++++++++------ 1 file changed, 63 insertions(+), 14 deletions(-) diff --git a/leptos/src/suspense_component.rs b/leptos/src/suspense_component.rs index e1e76b7cbc..242f52c98b 100644 --- a/leptos/src/suspense_component.rs +++ b/leptos/src/suspense_component.rs @@ -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 From 2480678603b1d49a3f5c2fb51f1151b01b116bf6 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Wed, 19 Nov 2025 19:56:44 -0500 Subject: [PATCH 85/91] fix: improve marker-node filtering when using islands router (closes #4443) (#4446) --- tachys/src/hydration.rs | 40 ++++++++++++++++++++++++++++++++----- tachys/src/view/any_view.rs | 16 +-------------- tachys/src/view/either.rs | 30 ++-------------------------- tachys/src/view/keyed.rs | 28 -------------------------- 4 files changed, 38 insertions(+), 76 deletions(-) diff --git a/tachys/src/hydration.rs b/tachys/src/hydration.rs index ba9aed73c1..8e393af0fd 100644 --- a/tachys/src/hydration.rs +++ b/tachys/src/hydration.rs @@ -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()); diff --git a/tachys/src/view/any_view.rs b/tachys/src/view/any_view.rs index 449937447e..8289b406e9 100644 --- a/tachys/src/view/any_view.rs +++ b/tachys/src/view/any_view.rs @@ -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"))] diff --git a/tachys/src/view/either.rs b/tachys/src/view/either.rs index 51a35e6b8a..a78aeacc5a 100644 --- a/tachys/src/view/either.rs +++ b/tachys/src/view/either.rs @@ -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 } } diff --git a/tachys/src/view/keyed.rs b/tachys/src/view/keyed.rs index acd499c25c..29f552526e 100644 --- a/tachys/src/view/keyed.rs +++ b/tachys/src/view/keyed.rs @@ -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, From 116b481cf66c5c322c9fb6e2c7c4205d798f1e1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=86gir=20=C3=96rn=20S=C3=ADmonarson?= <agirorn@gmail.com> Date: Thu, 20 Nov 2025 00:59:14 +0000 Subject: [PATCH 86/91] chore: removed duplicate workspace member `oco` (#4445) --- Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index e059b96af7..3105623135 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,6 @@ resolver = "2" members = [ # utilities - "oco", "any_spawner", "const_str_slice_concat", "either_of", From 0ebfe2b2353cf22ee44ef5497b6b3f74d5dfb94f Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Fri, 21 Nov 2025 13:16:24 -0500 Subject: [PATCH 87/91] fix: do not unescape query and hash in URLs when clicking links (closes (#4454) --- router/src/location/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/router/src/location/mod.rs b/router/src/location/mod.rs index d99ada30bd..18cef2c7b4 100644 --- a/router/src/location/mod.rs +++ b/router/src/location/mod.rs @@ -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| { From 71a09efbaccee93ed4a71e98a8aec5f9989973dc Mon Sep 17 00:00:00 2001 From: tqq1994516 <472159011@qq.com> Date: Sun, 23 Nov 2025 02:11:53 +0800 Subject: [PATCH 88/91] fix: add response headers for `leptos_axum` static files #4377 (#4394) --- examples/ssr_modes_axum/src/main.rs | 24 ++++++++++++++++++++++-- integrations/axum/src/lib.rs | 15 ++++++++++++++- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/examples/ssr_modes_axum/src/main.rs b/examples/ssr_modes_axum/src/main.rs index 2445cca75c..4d9eb38102 100644 --- a/examples/ssr_modes_axum/src/main.rs +++ b/examples/ssr_modes_axum/src/main.rs @@ -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 diff --git a/integrations/axum/src/lib.rs b/integrations/axum/src/lib.rs index 0930134d12..a7819b24ac 100644 --- a/integrations/axum/src/lib.rs +++ b/integrations/axum/src/lib.rs @@ -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 || { From dc6ebf233137662777f9c08f2b4d83f95e9fe14b Mon Sep 17 00:00:00 2001 From: Tyler Earls <tyler.a.earls@gmail.com> Date: Sat, 22 Nov 2025 12:12:10 -0600 Subject: [PATCH 89/91] fix: make class attribute overwrite behavior consistent between SSR and CSR (closes #4248) (#4439) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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> --- leptos/tests/ssr.rs | 70 ++++++++++++++++++++++++++++++++++++++++ tachys/src/html/class.rs | 26 +++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/leptos/tests/ssr.rs b/leptos/tests/ssr.rs index 9999899379..2b8e621089 100644 --- a/leptos/tests/ssr.rs +++ b/leptos/tests/ssr.rs @@ -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() { diff --git a/tachys/src/html/class.rs b/tachys/src/html/class.rs index ce3bf70c9d..3fcad11670 100644 --- a/tachys/src/html/class.rs +++ b/tachys/src/html/class.rs @@ -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, From 334d03b56e66c0fce77151fa8db35052f61f7e78 Mon Sep 17 00:00:00 2001 From: maccesch <maccesch@synphonyte.com> Date: Mon, 24 Nov 2025 06:40:00 +0100 Subject: [PATCH 90/91] Updated/reverted the Show... components --- leptos/src/animated_show.rs | 45 +++++++++---------------- leptos/src/show.rs | 59 +++------------------------------ leptos/src/show_let.rs | 66 +++---------------------------------- 3 files changed, 26 insertions(+), 144 deletions(-) diff --git a/leptos/src/animated_show.rs b/leptos/src/animated_show.rs index d91924755d..e31c36a9ef 100644 --- a/leptos/src/animated_show.rs +++ b/leptos/src/animated_show.rs @@ -1,18 +1,14 @@ -use crate::{ - children::ChildrenFn, component, control_flow::Show, show::IntoCondition, - IntoView, -}; +use crate::{children::ChildrenFn, component, control_flow::Show, IntoView}; use core::time::Duration; use leptos_dom::helpers::TimeoutHandle; use leptos_macro::view; use reactive_graph::{ - diagnostics::SpecialNonReactiveZone, effect::RenderEffect, owner::{on_cleanup, StoredValue}, signal::RwSignal, - traits::{GetValue, Set, SetValue}, + traits::{Get, GetUntracked, GetValue, Set, SetValue}, + wrappers::read::Signal, }; -use std::marker::PhantomData; use tachys::prelude::*; /// A component that will show its children when the `when` condition is `true`. @@ -54,12 +50,12 @@ use tachys::prelude::*; /// Please note, that unlike `Show`, `AnimatedShow` does not support a `fallback` prop. #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))] #[component] -pub fn AnimatedShow<M>( +pub fn AnimatedShow( /// The components Show wraps children: ChildrenFn, - /// When true the children are shown. - /// It accepts a closure that returns a boolean value as well as a boolean signal or plain boolean value. - when: impl IntoCondition<M>, + /// If the component should show or not + #[prop(into)] + when: Signal<bool>, /// Optional CSS class to apply if `when == true` #[prop(optional)] show_class: &'static str, @@ -68,26 +64,17 @@ pub fn AnimatedShow<M>( hide_class: &'static str, /// The timeout after which the component will be unmounted if `when == false` hide_delay: Duration, - - /// Marker for generic parameters. Ignore this. - #[prop(optional)] - _marker: PhantomData<M>, ) -> impl IntoView { - let when = when.into_condition(); - - // Silence warnings about using signals in non-reactive contexts. - #[cfg(debug_assertions)] - let z = SpecialNonReactiveZone::enter(); - let handle: StoredValue<Option<TimeoutHandle>> = StoredValue::new(None); - let cls = RwSignal::new(if when.run() { show_class } else { hide_class }); - let show = RwSignal::new(when.run()); - - #[cfg(debug_assertions)] - drop(z); + let cls = RwSignal::new(if when.get_untracked() { + show_class + } else { + hide_class + }); + let show = RwSignal::new(when.get_untracked()); let eff = RenderEffect::new(move |_| { - if when.run() { + if when.get() { // clear any possibly active timer if let Some(h) = handle.get_value() { h.clear(); @@ -115,8 +102,8 @@ pub fn AnimatedShow<M>( }); view! { - <Show when=show> - <div class=cls>{children()}</div> + <Show when=move || show.get() fallback=|| ()> + <div class=move || cls.get()>{children()}</div> </Show> } } diff --git a/leptos/src/show.rs b/leptos/src/show.rs index 3dc1766ab1..e22de52455 100644 --- a/leptos/src/show.rs +++ b/leptos/src/show.rs @@ -1,11 +1,9 @@ use crate::{ children::{TypedChildrenFn, ViewFn}, - prelude::FunctionMarker, IntoView, }; use leptos_macro::component; use reactive_graph::{computed::ArcMemo, traits::Get}; -use std::{marker::PhantomData, sync::Arc}; use tachys::either::Either; /// Shows its children whenever the condition `when` prop is `true`. @@ -47,25 +45,20 @@ use tachys::either::Either; /// # } /// ``` #[component] -pub fn Show<M, C>( +pub fn Show<W, C>( /// The children will be shown whenever the condition in the `when` closure returns `true`. children: TypedChildrenFn<C>, - /// When true the children are shown, otherwise the fallback. - /// It accepts a closure that returns a boolean value as well as a boolean signal or plain boolean value. - when: impl IntoCondition<M>, + /// A closure that returns a bool that determines whether this thing runs + when: W, /// A closure that returns what gets rendered if the when statement is false. By default this is the empty view. #[prop(optional, into)] fallback: ViewFn, - - /// Marker for generic parameters. Ignore this. - #[prop(optional)] - _marker: PhantomData<M>, ) -> impl IntoView where + W: Fn() -> bool + Send + Sync + 'static, C: IntoView + 'static, { - let when = when.into_condition(); - let memoized_when = ArcMemo::new(move |_| when.run()); + let memoized_when = ArcMemo::new(move |_| when()); let children = children.into_inner(); move || match memoized_when.get() { @@ -73,45 +66,3 @@ where false => Either::Right(fallback.run()), } } - -/// A closure that returns a bool. Can be converted from a closure, a signal, or a boolean value. -pub struct Condition(Arc<dyn Fn() -> bool + Send + Sync + 'static>); - -impl Condition { - /// Evaluates the condition and returns its result. - pub fn run(&self) -> bool { - (self.0)() - } -} - -/// Trait to convert various types into a `Condition`. -/// Implemented for closures, signals, and boolean values. -pub trait IntoCondition<M> { - /// Does the conversion - fn into_condition(self) -> Condition; -} - -#[cfg(not(feature = "nightly"))] -impl<S> IntoCondition<crate::prelude::SignalMarker> for S -where - S: Get<Value = bool> + Send + Sync + 'static, -{ - fn into_condition(self) -> Condition { - Condition(Arc::new(move || self.get())) - } -} - -impl<F> IntoCondition<FunctionMarker> for F -where - F: Fn() -> bool + Send + Sync + 'static, -{ - fn into_condition(self) -> Condition { - Condition(Arc::new(self)) - } -} - -impl IntoCondition<Condition> for Condition { - fn into_condition(self) -> Condition { - self - } -} diff --git a/leptos/src/show_let.rs b/leptos/src/show_let.rs index 0416d10c2f..cb3972cda4 100644 --- a/leptos/src/show_let.rs +++ b/leptos/src/show_let.rs @@ -72,7 +72,7 @@ use tachys::either::Either; /// # } /// ``` #[component] -pub fn ShowLet<T, ChFn, V, M>( +pub fn ShowLet<T, ChFn, V>( /// The children will be shown whenever `value` is `Some`. /// /// They take the inner value as an argument. Use `let:` to bind the value to a variable. @@ -81,7 +81,8 @@ pub fn ShowLet<T, ChFn, V, M>( /// A signal of type `Option` or a closure that returns an `Option`. /// If the value is `Some`, the children will be shown. /// Otherwise the fallback will be shown, if present. - some: impl IntoOptionGetter<T, M>, + #[prop(into)] + some: Signal<T>, /// A closure that returns what gets rendered when the value is `None`. /// By default this is the empty view. @@ -89,76 +90,19 @@ pub fn ShowLet<T, ChFn, V, M>( /// You can think of it as the closure inside `.unwrap_or_else(|| fallback())`. #[prop(optional, into)] fallback: ViewFn, - - /// Marker for generic parameters. Ignore this. - #[prop(optional)] - _marker: PhantomData<(T, M)>, ) -> impl IntoView where ChFn: Fn(T) -> V + Send + Clone + 'static, V: IntoView + 'static, T: 'static, { - let getter = some.into_option_getter(); - move || { let children = children.clone(); let fallback = fallback.clone(); - getter - .run() + some + .get() .map(move |t| Either::Left(children(t))) .unwrap_or_else(move || Either::Right(fallback.run())) } } - -/// Servers as a wrapper for both, an `Option` signal or a closure that returns an `Option`. -pub struct OptionGetter<T>(Arc<dyn Fn() -> Option<T> + Send + Sync + 'static>); - -impl<T> Clone for OptionGetter<T> { - fn clone(&self) -> Self { - Self(Arc::clone(&self.0)) - } -} - -impl<T> OptionGetter<T> { - /// Runs the getter and returns the result. - pub fn run(&self) -> Option<T> { - (self.0)() - } -} - -/// Conversion trait for creating an `OptionGetter` from a closure or a signal. -pub trait IntoOptionGetter<T, M> { - /// Converts the given value into an `OptionGetter`. - fn into_option_getter(self) -> OptionGetter<T>; -} - -/// Marker type for creating an `OptionGetter` from a closure. -/// Used so that the compiler doesn't complain about double implementations of the trait `IntoOptionGetter`. -pub struct FunctionMarker; - -impl<T, F> IntoOptionGetter<T, FunctionMarker> for F -where - F: Fn() -> Option<T> + Send + Sync + 'static, -{ - fn into_option_getter(self) -> OptionGetter<T> { - OptionGetter(Arc::new(self)) - } -} - -#[cfg(not(feature = "nightly"))] -/// Marker type for creating an `OptionGetter` from a signal. -/// Used so that the compiler doesn't complain about double implementations of the trait `IntoOptionGetter`. -pub struct SignalMarker; - -#[cfg(not(feature = "nightly"))] -impl<T, S> IntoOptionGetter<T, SignalMarker> for S -where - S: Get<Value = Option<T>> + Clone + Send + Sync + 'static, -{ - fn into_option_getter(self) -> OptionGetter<T> { - let cloned = self.clone(); - OptionGetter(Arc::new(move || cloned.get())) - } -} From 914c3c479d2f81f3863e5278594ed12478258ac9 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 05:42:24 +0000 Subject: [PATCH 91/91] [autofix.ci] apply automated fixes --- leptos/src/show_let.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/leptos/src/show_let.rs b/leptos/src/show_let.rs index cb3972cda4..392fcf0c0e 100644 --- a/leptos/src/show_let.rs +++ b/leptos/src/show_let.rs @@ -100,8 +100,7 @@ where let children = children.clone(); let fallback = fallback.clone(); - some - .get() + some.get() .map(move |t| Either::Left(children(t))) .unwrap_or_else(move || Either::Right(fallback.run())) }