From f8fd79725a15cda3650d386165e9fa5a60f94678 Mon Sep 17 00:00:00 2001 From: Nesterov Nikita Date: Tue, 15 Jul 2025 09:14:57 +0300 Subject: [PATCH 01/24] fix(hot-reload): parse RawText node --- leptos_hot_reload/src/node.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/leptos_hot_reload/src/node.rs b/leptos_hot_reload/src/node.rs index 78dd09429..9ceae5c56 100644 --- a/leptos_hot_reload/src/node.rs +++ b/leptos_hot_reload/src/node.rs @@ -66,6 +66,9 @@ impl LNode { LNode::parse_node(child, views)?; } } + Node::RawText(text) => { + views.push(LNode::Text(text.to_string_best())); + } Node::Text(text) => { views.push(LNode::Text(text.value_string())); } From afb37aaf4b36d7301e2b43223cb51d5004d52c89 Mon Sep 17 00:00:00 2001 From: Nesterov Nikita Date: Tue, 15 Jul 2025 09:28:59 +0300 Subject: [PATCH 02/24] fix(hot-reload): handle DOM-less views --- tachys/src/view/any_view.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tachys/src/view/any_view.rs b/tachys/src/view/any_view.rs index 55b729efb..42168ad95 100644 --- a/tachys/src/view/any_view.rs +++ b/tachys/src/view/any_view.rs @@ -217,7 +217,7 @@ where extra_attrs, ); if !T::EXISTS { - buf.push_str(""); + buf.push_str(""); } } @@ -238,7 +238,7 @@ where extra_attrs, ); if !T::EXISTS { - buf.push_sync(""); + buf.push_sync(""); } } @@ -259,7 +259,7 @@ where extra_attrs, ); if !T::EXISTS { - buf.push_sync(""); + buf.push_sync(""); } } From 5fc56346f468787fe917f2540964dece9c916b6a Mon Sep 17 00:00:00 2001 From: Nesterov Nikita Date: Tue, 15 Jul 2025 09:30:30 +0300 Subject: [PATCH 03/24] chore: format patch.js with prettier --- leptos_hot_reload/src/patch.js | 165 ++++++++++++++++++++++++++------- 1 file changed, 131 insertions(+), 34 deletions(-) diff --git a/leptos_hot_reload/src/patch.js b/leptos_hot_reload/src/patch.js index b99cdd858..2846ebfdc 100644 --- a/leptos_hot_reload/src/patch.js +++ b/leptos_hot_reload/src/patch.js @@ -1,10 +1,15 @@ -console.log("[HOT RELOADING] Connected to server.\n\nNote: `cargo-leptos watch --hot-reload` only works with the `nightly` feature enabled on Leptos."); +console.log( + "[HOT RELOADING] Connected to server.\n\nNote: `cargo-leptos watch --hot-reload` only works with the `nightly` feature enabled on Leptos.", +); function patch(json) { try { const views = JSON.parse(json); for (const [id, patches] of views) { console.log("[HOT RELOAD]", id, patches); - const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT), + const walker = document.createTreeWalker( + document.body, + NodeFilter.SHOW_COMMENT, + ), open = `hot-reload|${id}|open`, close = `hot-reload|${id}|close`; let start, end; @@ -22,14 +27,20 @@ function patch(json) { for (const [start, end] of instances) { // build tree of current actual children - const actualChildren = childrenFromRange(start.parentElement, start, end); + const actualChildren = childrenFromRange( + start.parentElement, + start, + end, + ); const actions = []; // build up the set of actions for (const patch of patches) { const child = childAtPath( - actualChildren.length > 1 ? { children: actualChildren } : actualChildren[0], - patch.path + actualChildren.length > 1 + ? { children: actualChildren } + : actualChildren[0], + patch.path, ); const action = patch.action; if (action == "ClearChildren") { @@ -39,8 +50,15 @@ function patch(json) { }); } else if (action.ReplaceWith) { actions.push(() => { - console.log("[HOT RELOAD] > ReplaceWith", child, action.ReplaceWith); - const replacement = fromReplacementNode(action.ReplaceWith, actualChildren); + console.log( + "[HOT RELOAD] > ReplaceWith", + child, + action.ReplaceWith, + ); + const replacement = fromReplacementNode( + action.ReplaceWith, + actualChildren, + ); if (child.node) { child.node.replaceWith(replacement); } else { @@ -54,7 +72,11 @@ function patch(json) { } else if (action.ChangeTagName) { const oldNode = child.node; actions.push(() => { - console.log("[HOT RELOAD] > ChangeTagName", child.node, action.ChangeTagName); + console.log( + "[HOT RELOAD] > ChangeTagName", + child.node, + action.ChangeTagName, + ); const newElement = document.createElement(action.ChangeTagName); for (const attr of oldNode.attributes) { newElement.setAttribute(attr.name, attr.value); @@ -67,13 +89,21 @@ function patch(json) { }); } else if (action.RemoveAttribute) { actions.push(() => { - console.log("[HOT RELOAD] > RemoveAttribute", child.node, action.RemoveAttribute); + console.log( + "[HOT RELOAD] > RemoveAttribute", + child.node, + action.RemoveAttribute, + ); child.node.removeAttribute(action.RemoveAttribute); }); } else if (action.SetAttribute) { const [name, value] = action.SetAttribute; actions.push(() => { - console.log("[HOT RELOAD] > SetAttribute", child.node, action.SetAttribute); + console.log( + "[HOT RELOAD] > SetAttribute", + child.node, + action.SetAttribute, + ); child.node.setAttribute(name, value); }); } else if (action.SetText) { @@ -84,13 +114,25 @@ function patch(json) { }); } else if (action.AppendChildren) { actions.push(() => { - console.log("[HOT RELOAD] > AppendChildren", child.node, action.AppendChildren); - const newChildren = fromReplacementNode(action.AppendChildren, actualChildren); + console.log( + "[HOT RELOAD] > AppendChildren", + child.node, + action.AppendChildren, + ); + const newChildren = fromReplacementNode( + action.AppendChildren, + actualChildren, + ); child.node.append(newChildren); }); } else if (action.RemoveChild) { actions.push(() => { - console.log("[HOT RELOAD] > RemoveChild", child.node, child.children, action.RemoveChild); + console.log( + "[HOT RELOAD] > RemoveChild", + child.node, + child.children, + action.RemoveChild, + ); const toRemove = child.children[action.RemoveChild.at]; let toRemoveNode = toRemove.node; if (!toRemoveNode) { @@ -103,33 +145,56 @@ function patch(json) { } }); } else if (action.InsertChild) { - const newChild = fromReplacementNode(action.InsertChild.child, actualChildren); + const newChild = fromReplacementNode( + action.InsertChild.child, + actualChildren, + ); let children = []; if (child.children) { children = child.children; } else if (child.start && child.end) { - children = childrenFromRange(child.node || child.start.parentElement, start, end); + children = childrenFromRange( + child.node || child.start.parentElement, + start, + end, + ); } else { console.warn("InsertChildAfter could not build children."); } const before = children[action.InsertChild.before]; actions.push(() => { - console.log("[HOT RELOAD] > InsertChild", child, child.node, action.InsertChild, " before ", before); + console.log( + "[HOT RELOAD] > InsertChild", + child, + child.node, + action.InsertChild, + " before ", + before, + ); if (!before && child.node) { child.node.appendChild(newChild); } else { let node = child.node || child.end.parentElement; - const reference = before ? before.node || before.start : child.end; + const reference = before + ? before.node || before.start + : child.end; node.insertBefore(newChild, reference); } }); } else if (action.InsertChildAfter) { - const newChild = fromReplacementNode(action.InsertChildAfter.child, actualChildren); + const newChild = fromReplacementNode( + action.InsertChildAfter.child, + actualChildren, + ); let children = []; if (child.children) { children = child.children; } else if (child.start && child.end) { - children = childrenFromRange(child.node || child.start.parentElement, start, end); + children = childrenFromRange( + child.node || child.start.parentElement, + start, + end, + ); } else { console.warn("InsertChildAfter could not build children."); } @@ -141,17 +206,24 @@ function patch(json) { child.node, action.InsertChildAfter, " after ", - after + after, ); - if (child.node && (!after || !(after.node || after.start).nextSibling)) { + if ( + child.node && + (!after || !(after.node || after.start).nextSibling) + ) { child.node.appendChild(newChild); } else { const node = child.node || child.end; - const parent = node.nodeType === Node.COMMENT_NODE ? node.parentNode : node; + const parent = + node.nodeType === Node.COMMENT_NODE ? node.parentNode : node; if (!after) { parent.appendChild(newChild); } else { - parent.insertBefore(newChild, (after.node || after.start).nextSibling); + parent.insertBefore( + newChild, + (after.node || after.start).nextSibling, + ); } } }); @@ -191,8 +263,10 @@ function patch(json) { return element; } else { const child = childAtPath( - actualChildren.length > 1 ? { children: actualChildren } : actualChildren[0], - node.Path + actualChildren.length > 1 + ? { children: actualChildren } + : actualChildren[0], + node.Path, ); if (child) { let childNode = child.node; @@ -215,7 +289,10 @@ function patch(json) { } return childNode; } else { - console.warn("[HOT RELOADING] Could not find replacement node at ", node.Path); + console.warn( + "[HOT RELOADING] Could not find replacement node at ", + node.Path, + ); return undefined; } } @@ -227,13 +304,16 @@ function patch(json) { NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT, { acceptNode(node) { - if (node.parentNode == element && (!range || range.isPointInRange(node, 0))) { + if ( + node.parentNode == element && + (!range || range.isPointInRange(node, 0)) + ) { return NodeFilter.FILTER_ACCEPT; } else { return NodeFilter.FILTER_REJECT; } }, - } + }, ); const actualChildren = [], elementCount = {}; @@ -260,9 +340,13 @@ function patch(json) { }); } else if (walker.currentNode.nodeType == Node.COMMENT_NODE) { if (walker.currentNode.textContent.trim().startsWith("hot-reload")) { - if (walker.currentNode.textContent.trim().endsWith("-children|open")) { + if ( + walker.currentNode.textContent.trim().endsWith("-children|open") + ) { const startingName = walker.currentNode.textContent.trim(); - const componentName = startingName.replace("-children|open").replace("hot-reload|"); + const componentName = startingName + .replace("-children|open") + .replace("hot-reload|"); const endingName = `hot-reload|${componentName}-children|close`; let start = walker.currentNode; let depth = 1; @@ -270,7 +354,9 @@ function patch(json) { while (walker.nextNode()) { if (walker.currentNode.textContent.trim() == endingName) { depth--; - } else if (walker.currentNode.textContent.trim() == startingName) { + } else if ( + walker.currentNode.textContent.trim() == startingName + ) { depth++; } @@ -283,7 +369,11 @@ function patch(json) { type: "fragment", start: start.nextSibling, end: end.previousSibling, - children: childrenFromRange(start.parentElement, start.nextSibling, end.previousSibling), + children: childrenFromRange( + start.parentElement, + start.nextSibling, + end.previousSibling, + ), }); } } else if (walker.currentNode.textContent.trim() == "<() />") { @@ -358,7 +448,10 @@ function patch(json) { }); } } else { - console.warn("[HOT RELOADING] Building children, encountered", walker.currentNode); + console.warn( + "[HOT RELOADING] Building children, encountered", + walker.currentNode, + ); } } return actualChildren; @@ -374,7 +467,11 @@ function patch(json) { } else if (path == [0]) { return element; } else if (element.start && element.end) { - const actualChildren = childrenFromRange(element.node || element.start.parentElement, element.start, element.end); + const actualChildren = childrenFromRange( + element.node || element.start.parentElement, + element.start, + element.end, + ); return childAtPath({ children: actualChildren }, path); } else { console.warn("[HOT RELOADING] Child at ", path, "not found in ", element); From 33b278c0149cf6a89d1df7091d88e3124a3bb6f4 Mon Sep 17 00:00:00 2001 From: Nesterov Nikita Date: Tue, 15 Jul 2025 09:35:06 +0300 Subject: [PATCH 04/24] fix(hot-reload): fragments were not walked over properly --- leptos_hot_reload/src/patch.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/leptos_hot_reload/src/patch.js b/leptos_hot_reload/src/patch.js index 2846ebfdc..0266f0646 100644 --- a/leptos_hot_reload/src/patch.js +++ b/leptos_hot_reload/src/patch.js @@ -339,15 +339,13 @@ function patch(json) { node: walker.currentNode, }); } else if (walker.currentNode.nodeType == Node.COMMENT_NODE) { - if (walker.currentNode.textContent.trim().startsWith("hot-reload")) { - if ( - walker.currentNode.textContent.trim().endsWith("-children|open") - ) { + if (walker.currentNode.textContent.trim().startsWith("hot-reload|")) { + if (walker.currentNode.textContent.trim().endsWith("|open")) { const startingName = walker.currentNode.textContent.trim(); const componentName = startingName - .replace("-children|open") - .replace("hot-reload|"); - const endingName = `hot-reload|${componentName}-children|close`; + .replace("|open", "") + .replace("hot-reload|", ""); + const endingName = `hot-reload|${componentName}|close`; let start = walker.currentNode; let depth = 1; From 972b1ff90b3df21368c5ac10553520e611e08d27 Mon Sep 17 00:00:00 2001 From: mahdi739 <86552031+mahdi739@users.noreply.github.com> Date: Fri, 18 Jul 2025 17:54:32 +0330 Subject: [PATCH 05/24] feat: support conversion from signals and optional get extension for TextProp (#4159) * feat: support conversion from signals and optional get extension for TextProp * [autofix.ci] apply automated fixes * remove unused import --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- leptos/src/text_prop.rs | 92 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 91 insertions(+), 1 deletion(-) diff --git a/leptos/src/text_prop.rs b/leptos/src/text_prop.rs index 570ecd161..dc13c895c 100644 --- a/leptos/src/text_prop.rs +++ b/leptos/src/text_prop.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use tachys::prelude::IntoAttributeValue; /// Describes a value that is either a static or a reactive string, i.e., -/// a [`String`], a [`&str`], or a reactive `Fn() -> String`. +/// a [`String`], a [`&str`], a `Signal` or a reactive `Fn() -> String`. #[derive(Clone)] pub struct TextProp(Arc Oco<'static, str> + Send + Sync>); @@ -82,3 +82,93 @@ impl IntoAttributeValue for TextProp { self.0 } } + +macro_rules! textprop_reactive { + ($name:ident, <$($gen:ident),*>, $v:ty, $( $where_clause:tt )*) => + { + #[allow(deprecated)] + impl<$($gen),*> From<$name<$($gen),*>> for TextProp + where + $v: Into> + Clone + Send + Sync + 'static, + $($where_clause)* + { + #[inline(always)] + fn from(s: $name<$($gen),*>) -> Self { + TextProp(Arc::new(move || s.get().into())) + } + } + }; +} + +#[cfg(not(feature = "nightly"))] +mod stable { + use super::TextProp; + use oco_ref::Oco; + #[allow(deprecated)] + use reactive_graph::wrappers::read::MaybeSignal; + use reactive_graph::{ + computed::{ArcMemo, Memo}, + owner::Storage, + signal::{ArcReadSignal, ArcRwSignal, ReadSignal, RwSignal}, + traits::Get, + wrappers::read::{ArcSignal, Signal}, + }; + use std::sync::Arc; + + textprop_reactive!( + RwSignal, + , + V, + RwSignal: Get, + S: Storage + Storage>, + S: Send + Sync + 'static, + ); + textprop_reactive!( + ReadSignal, + , + V, + ReadSignal: Get, + S: Storage + Storage>, + S: Send + Sync + 'static, + ); + textprop_reactive!( + Memo, + , + V, + Memo: Get, + S: Storage + Storage>, + S: Send + Sync + 'static, + ); + textprop_reactive!( + Signal, + , + V, + Signal: Get, + S: Storage + Storage>, + S: Send + Sync + 'static, + ); + textprop_reactive!( + MaybeSignal, + , + V, + MaybeSignal: Get, + S: Storage + Storage>, + S: Send + Sync + 'static, + ); + textprop_reactive!(ArcRwSignal, , V, ArcRwSignal: Get); + textprop_reactive!(ArcReadSignal, , V, ArcReadSignal: Get); + textprop_reactive!(ArcMemo, , V, ArcMemo: Get); + textprop_reactive!(ArcSignal, , V, ArcSignal: Get); +} + +/// Extension trait for `Option` +pub trait OptionTextPropExt { + /// Accesses the current value of the `Option` as an `Option>`. + fn get(&self) -> Option>; +} + +impl OptionTextPropExt for Option { + fn get(&self) -> Option> { + self.as_ref().map(|text_prop| text_prop.get()) + } +} From 2817a261ce98e217442e7928e9f5a837852adf30 Mon Sep 17 00:00:00 2001 From: TERRORW0LF <61637480+TERRORW0LF@users.noreply.github.com> Date: Fri, 18 Jul 2025 16:25:17 +0200 Subject: [PATCH 06/24] docs: add warning for reading hash on the server (#4158) --- router/src/location/mod.rs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/router/src/location/mod.rs b/router/src/location/mod.rs index 14ff2b979..faa361472 100644 --- a/router/src/location/mod.rs +++ b/router/src/location/mod.rs @@ -67,10 +67,32 @@ impl Url { } pub fn hash(&self) -> &str { + #[cfg(all(feature = "ssr", any(debug_assertions, leptos_debuginfo)))] + { + #[cfg(feature = "tracing")] + tracing::warn!( + "Reading hash on the server can lead to hydration errors." + ); + #[cfg(not(feature = "tracing"))] + eprintln!( + "Reading hash on the server can lead to hydration errors." + ); + } &self.hash } pub fn hash_mut(&mut self) -> &mut String { + #[cfg(all(feature = "ssr", any(debug_assertions, leptos_debuginfo)))] + { + #[cfg(feature = "tracing")] + tracing::warn!( + "Reading hash on the server can lead to hydration errors." + ); + #[cfg(not(feature = "tracing"))] + eprintln!( + "Reading hash on the server can lead to hydration errors." + ); + } &mut self.hash } @@ -173,7 +195,7 @@ impl Location { let state = state.into(); let pathname = Memo::new(move |_| url.with(|url| url.path.clone())); let search = Memo::new(move |_| url.with(|url| url.search.clone())); - let hash = Memo::new(move |_| url.with(|url| url.hash.clone())); + let hash = Memo::new(move |_| url.with(|url| url.hash().to_string())); let query = Memo::new(move |_| url.with(|url| url.search_params.clone())); Location { From 66d1bead9ab79f89f5800a32eff74c96546fc6dd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Jul 2025 10:32:18 -0400 Subject: [PATCH 07/24] chore(deps): bump the rust-dependencies group across 1 directory with 15 updates (#4152) --- updated-dependencies: - dependency-name: trybuild dependency-version: 1.0.106 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: tokio dependency-version: 1.46.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: rust-dependencies - dependency-name: config dependency-version: 0.15.13 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: const-str dependency-version: 0.6.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: reqwest dependency-version: 0.12.22 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: cc dependency-version: 1.2.29 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: cfg-expr dependency-version: 0.20.1 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: collection_literals dependency-version: 1.0.2 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: crc32fast dependency-version: 1.5.0 dependency-type: indirect update-type: version-update:semver-minor dependency-group: rust-dependencies - dependency-name: h2 dependency-version: 0.3.27 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: hyper-util dependency-version: 0.1.15 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: rustls dependency-version: 0.23.29 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: rustls-webpki dependency-version: 0.103.4 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: sdd dependency-version: 3.0.9 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: winnow dependency-version: 0.7.12 dependency-type: indirect update-type: version-update:semver-patch dependency-group: rust-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 136 +++++++++++++++++++++++++++++++++++++---------------- Cargo.toml | 10 ++-- 2 files changed, 100 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 78992ad91..238e7cc9b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -565,9 +565,9 @@ checksum = "0da45bc31171d8d6960122e222a67740df867c1dd53b4d51caa297084c185cab" [[package]] name = "cc" -version = "1.2.27" +version = "1.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" +checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362" dependencies = [ "jobserver", "libc", @@ -576,9 +576,9 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e34e221e91c7eb5e8315b5c9cf1a61670938c0626451f954a51693ed44b37f45" +checksum = "0d0390889d58f934f01cd49736275b4c2da15bcfc328c78ff2349907e6cabf22" dependencies = [ "smallvec", "target-lexicon", @@ -651,9 +651,9 @@ dependencies = [ [[package]] name = "collection_literals" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186dce98367766de751c42c4f03970fc60fc012296e706ccbb9d5df9b6c1e271" +checksum = "26b3f65b8fb8e88ba339f7d23a390fe1b0896217da05e2a66c584c9b29a91df8" [[package]] name = "concurrent-queue" @@ -666,14 +666,14 @@ dependencies = [ [[package]] name = "config" -version = "0.15.11" +version = "0.15.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "595aae20e65c3be792d05818e8c63025294ac3cb7e200f11459063a352a6ef80" +checksum = "5b1eb4fb07bc7f012422df02766c7bd5971effb894f573865642f06fa3265440" dependencies = [ "convert_case 0.6.0", "pathdiff", "serde", - "toml", + "toml 0.9.2", "winnow", ] @@ -691,9 +691,9 @@ dependencies = [ [[package]] name = "const-str" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e991226a70654b49d34de5ed064885f0bef0348a8e70018b8ff1ac80aa984a2" +checksum = "041fbfcf8e7054df725fb9985297e92422cdc80fcf313665f5ca3d761bb63f4c" [[package]] name = "const_format" @@ -781,9 +781,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -1315,9 +1315,9 @@ checksum = "17e2ac29387b1aa07a1e448f7bb4f35b500787971e965b02842b900afa5c8f6f" [[package]] name = "h2" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" dependencies = [ "bytes", "fnv", @@ -1535,9 +1535,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" +checksum = "7f66d5bd4c6f02bf0542fad85d626775bab9258cf795a4256dcaf3161114d1df" dependencies = [ "base64", "bytes", @@ -1706,6 +1706,17 @@ dependencies = [ "rustversion", ] +[[package]] +name = "io-uring" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -2826,9 +2837,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.20" +version = "0.12.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813" +checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" dependencies = [ "base64", "bytes", @@ -2992,9 +3003,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.28" +version = "0.23.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" +checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1" dependencies = [ "once_cell", "ring", @@ -3016,9 +3027,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.3" +version = "0.103.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" dependencies = [ "ring", "rustls-pki-types", @@ -3072,9 +3083,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sdd" -version = "3.0.8" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "584e070911c7017da6cb2eb0788d09f43d789029b5877d3e5ecc8acf86ceee21" +checksum = "62f5557d2bbddd5afd236ba7856b0e494f5acc7ce805bb0774cc5674b20a06b4" [[package]] name = "security-framework" @@ -3208,6 +3219,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -3510,7 +3530,7 @@ dependencies = [ "cfg-expr", "heck", "pkg-config", - "toml", + "toml 0.8.23", "version-compare", ] @@ -3702,17 +3722,19 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.45.1" +version = "1.46.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" dependencies = [ "backtrace", "bytes", + "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", + "slab", "socket2", "tokio-macros", "windows-sys 0.52.0", @@ -3818,11 +3840,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", "toml_edit", ] +[[package]] +name = "toml" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed0aee96c12fa71097902e0bb061a5e1ebd766a6636bb605ba401c45c1650eac" +dependencies = [ + "indexmap", + "serde", + "serde_spanned 1.0.0", + "toml_datetime 0.7.0", + "toml_parser", + "toml_writer", + "winnow", +] + [[package]] name = "toml_datetime" version = "0.6.11" @@ -3832,6 +3869,15 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" +dependencies = [ + "serde", +] + [[package]] name = "toml_edit" version = "0.22.27" @@ -3840,17 +3886,25 @@ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", - "serde_spanned", - "toml_datetime", - "toml_write", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", "winnow", ] [[package]] -name = "toml_write" -version = "0.1.2" +name = "toml_parser" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +checksum = "97200572db069e74c512a14117b296ba0a80a30123fbbb5aa1f4a348f639ca30" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" [[package]] name = "tower" @@ -3948,9 +4002,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "trybuild" -version = "1.0.105" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c9bf9513a2f4aeef5fdac8677d7d349c79fdbcc03b9c86da6e9d254f1e43be2" +checksum = "65af40ad689f2527aebbd37a0a816aea88ff5f774ceabe99de5be02f2f91dae2" dependencies = [ "glob", "serde", @@ -3958,7 +4012,7 @@ dependencies = [ "serde_json", "target-triple", "termcolor", - "toml", + "toml 0.9.2", ] [[package]] @@ -4452,9 +4506,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 3d4f44ae7..872b32a7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,7 +76,7 @@ tachys = { path = "./tachys", version = "0.2.4" } 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.140" } -trybuild = { default-features = false, version = "1.0.105" } +trybuild = { default-features = false, version = "1.0.106" } typed-builder = { default-features = false, version = "0.21.0" } thiserror = { default-features = false, version = "2.0.12" } wasm-bindgen = { default-features = false, version = "0.2.100" } @@ -98,7 +98,7 @@ proc-macro-error2 = { default-features = false, version = "2.0.1" } const_format = { default-features = false, version = "0.2.34" } gloo-net = { default-features = false, version = "0.6.0" } url = { default-features = false, version = "2.5.4" } -tokio = { default-features = false, version = "1.45.1" } +tokio = { default-features = false, version = "1.46.1" } base64 = { default-features = false, version = "0.22.1" } cfg-if = { default-features = false, version = "1.0.0" } wasm-bindgen-futures = { default-features = false, version = "0.4.50" } @@ -129,7 +129,7 @@ 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.35" } inventory = { default-features = false, version = "0.3.20" } -config = { default-features = false, version = "0.15.11" } +config = { default-features = false, version = "0.15.13" } camino = { default-features = false, version = "1.1.9" } ciborium = { default-features = false, version = "0.2.2" } multer = { default-features = false, version = "3.1.0" } @@ -149,12 +149,12 @@ futures-lite = { default-features = false, version = "2.6.0" } log = { default-features = false, version = "0.4.27" } percent-encoding = { default-features = false, version = "2.3.1" } async-executor = { default-features = false, version = "1.13.2" } -const-str = { default-features = false, version = "0.6.2" } +const-str = { default-features = false, version = "0.6.3" } http-body-util = { default-features = false, version = "0.1.3" } hyper = { default-features = false, version = "1.6.0" } postcard = { default-features = false, version = "1.1.1" } rmp-serde = { default-features = false, version = "1.3.0" } -reqwest = { default-features = false, version = "0.12.18" } +reqwest = { default-features = false, version = "0.12.22" } 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" } From 17d357bcec84189409dbb090a1335c31c9b27582 Mon Sep 17 00:00:00 2001 From: Rakshith Ravi <12864597+rakshith-ravi@users.noreply.github.com> Date: Fri, 18 Jul 2025 20:03:45 +0530 Subject: [PATCH 08/24] chore(README): we're kinda prod-ready (#4148) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1edc68bba..78dc9d13c 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ Yes, I’m sure there are. You can see from the state of our issue tracker over This may be the big one: “production ready” implies a certain orientation to a library: that you can basically use it, without any special knowledge of its internals or ability to contribute. Everyone has this at some level in their stack: for example I (@gbj) don’t have the capacity or knowledge to contribute to something like `wasm-bindgen` at this point: I simply rely on it to work. -There are several people in the community using Leptos right now for internal apps at work, who have also become significant contributors. I think this is the right level of production use for now. There may be missing features that you need, and you may end up building them! But for internal apps, if you’re willing to build and contribute missing pieces along the way, the framework is definitely usable right now. +There are several people in the community using Leptos right now for many websites at work, who have also become significant contributors. There may be missing features that you need, and you may end up building them! But, if you're willing to contribute a few missing pieces along the way, the framework is most definitely usable for production applications, especially given the ecosystem of libraries that have sprung up around it. ### Can I use this for native GUI? From d7f4457ea4639697ce122abe45310de6c7052f42 Mon Sep 17 00:00:00 2001 From: Nesterov Nikita Date: Sat, 19 Jul 2025 07:39:32 +0300 Subject: [PATCH 09/24] feat(hot-reload): implement Myers diffing algorithm --- leptos_hot_reload/src/diff.rs | 163 ++++++++++++++-------------------- 1 file changed, 65 insertions(+), 98 deletions(-) diff --git a/leptos_hot_reload/src/diff.rs b/leptos_hot_reload/src/diff.rs index 347ee188e..a12d6d892 100644 --- a/leptos_hot_reload/src/diff.rs +++ b/leptos_hot_reload/src/diff.rs @@ -251,93 +251,66 @@ impl LNode { action: PatchAction::ClearChildren, }] } else { - let mut a = 0; - let mut b = std::cmp::max(old.len(), new.len()) - 1; // min is 0, have checked both have items + let width = old.len() + 1; + let height = new.len() + 1; + let mut mat = vec![0; width * height]; + for i in 1..width { + mat[i] = i; + } + for i in 1..height { + mat[i * width] = i; + } + for j in 1..height { + for i in 1..width { + if old[i - 1] == new[j - 1] { + mat[j * width + i] = mat[(j - 1) * width + (i - 1)]; + } else { + mat[j * width + i] = (mat[(j - 1) * width + i] + 1) + .min(mat[j * width + (i - 1)] + 1) + .min(mat[(j - 1) * width + (i - 1)] + 1) + } + } + } + let (mut i, mut j) = (old.len(), new.len()); let mut patches = vec![]; - // common prefix - while a < b { - let old = old.get(a); - let new = new.get(a); - - match (old, new) { - (None, Some(new)) => patches.push(Patch { - path: path.to_owned(), - action: PatchAction::InsertChild { - before: a, - child: new.to_replacement_node(old_children), - }, - }), - (Some(_), None) => patches.push(Patch { - path: path.to_owned(), - action: PatchAction::RemoveChild { at: a }, - }), - (Some(old), Some(new)) if old != new => { - break; - } - _ => {} - } - - a += 1; - } - - // common suffix - while b >= a { - let old = old.get(b); - let new = new.get(b); - - match (old, new) { - (None, Some(new)) => patches.push(Patch { - path: path.to_owned(), - action: PatchAction::InsertChildAfter { - after: b - 1, - child: new.to_replacement_node(old_children), - }, - }), - (Some(_), None) => patches.push(Patch { - path: path.to_owned(), - action: PatchAction::RemoveChild { at: b }, - }), - (Some(old), Some(new)) if old != new => { - break; - } - _ => {} - } - - if b == 0 { - break; - } - b -= 1; - } - - // diffing in middle - if b >= a { - let old_slice_end = - if b >= old.len() { old.len() - 1 } else { b }; - let new_slice_end = - if b >= new.len() { new.len() - 1 } else { b }; - let old = &old[a..=old_slice_end]; - let new = &new[a..=new_slice_end]; - - for (new_idx, new_node) in new.iter().enumerate() { - match old.get(new_idx) { - Some(old_node) => { - let mut new_path = path.to_vec(); - new_path.push(new_idx + a); - let diffs = old_node.diff_at( - new_node, - &new_path, - old_children, - ); - patches.extend(&mut diffs.into_iter()); - } - None => patches.push(Patch { + while i > 0 || j > 0 { + if i > 0 && j > 0 && old[i - 1] == new[j - 1] { + i -= 1; + j -= 1; + } else { + let current = mat[j * width + i]; + if i > 0 + && j > 0 + && mat[(j - 1) * width + i - 1] + 1 == current + { + let mut new_path = path.to_owned(); + new_path.push(i - 1); + let diffs = old[i - 1].diff_at( + &new[j - 1], + &new_path, + old_children, + ); + patches.extend(&mut diffs.into_iter()); + i -= 1; + j -= 1; + } else if i > 0 && mat[j * width + i - 1] + 1 == current { + patches.push(Patch { + path: path.to_owned(), + action: PatchAction::RemoveChild { at: i - 1 }, + }); + i -= 1; + } else if j > 0 && mat[(j - 1) * width + i] + 1 == current { + patches.push(Patch { path: path.to_owned(), action: PatchAction::InsertChild { - before: new_idx, - child: new_node - .to_replacement_node(old_children), + before: i, + child: new[j - 1] + .to_replacement_node(&old_children), }, - }), + }); + j -= 1; + } else { + unreachable!(); } } } @@ -514,23 +487,17 @@ mod tests { let delta = a.diff(&b); assert_eq!( delta, - vec![ - Patch { - path: vec![], - action: PatchAction::InsertChildAfter { - after: 0, - child: ReplacementNode::Element { - name: "button".into(), - attrs: vec![], - children: vec![ReplacementNode::Html("bar".into())] - } + vec![Patch { + path: vec![], + action: PatchAction::InsertChild { + before: 0, + child: ReplacementNode::Element { + name: "button".into(), + attrs: vec![], + children: vec![ReplacementNode::Html("foo".into())] } - }, - Patch { - path: vec![0, 0], - action: PatchAction::SetText("foo".into()) } - ] + }] ); } From bd454d03e282972336323290b664a329b023f25f Mon Sep 17 00:00:00 2001 From: Nesterov Nikita Date: Sat, 19 Jul 2025 08:05:11 +0300 Subject: [PATCH 10/24] refactor(hot-reload): immediately apply patches --- leptos_hot_reload/src/patch.js | 258 +++++++++++++++------------------ 1 file changed, 114 insertions(+), 144 deletions(-) diff --git a/leptos_hot_reload/src/patch.js b/leptos_hot_reload/src/patch.js index 3b5ed4e3c..af49904d9 100644 --- a/leptos_hot_reload/src/patch.js +++ b/leptos_hot_reload/src/patch.js @@ -23,15 +23,11 @@ function patch(json) { } for (const [start, end] of instances) { - // build tree of current actual children const actualChildren = childrenFromRange( start.parentElement, start, end, ); - const actions = []; - - // build up the set of actions for (const patch of patches) { const child = childAtPath( actualChildren.length > 1 @@ -41,106 +37,90 @@ function patch(json) { ); const action = patch.action; if (action == "ClearChildren") { - actions.push(() => { - console.log("[HOT RELOAD] > ClearChildren", child.node); - child.node.textContent = ""; - }); + console.log("[HOT RELOAD] > ClearChildren", child.node); + child.node.textContent = ""; } else if (action.ReplaceWith) { - actions.push(() => { - console.log( - "[HOT RELOAD] > ReplaceWith", - child, - action.ReplaceWith, - ); - const replacement = fromReplacementNode( - action.ReplaceWith, - actualChildren, - ); - if (child.node) { - child.node.replaceWith(replacement); - } else { - const range = new Range(); - range.setStartAfter(child.start); - range.setEndAfter(child.end); - range.deleteContents(); - child.start.replaceWith(replacement); - } - }); + console.log( + "[HOT RELOAD] > ReplaceWith", + child, + action.ReplaceWith, + ); + const replacement = fromReplacementNode( + action.ReplaceWith, + actualChildren, + ); + if (child.node) { + child.node.replaceWith(replacement); + } else { + const range = new Range(); + range.setStartAfter(child.start); + range.setEndAfter(child.end); + range.deleteContents(); + child.start.replaceWith(replacement); + } } else if (action.ChangeTagName) { const oldNode = child.node; - actions.push(() => { - console.log( - "[HOT RELOAD] > ChangeTagName", - child.node, - action.ChangeTagName, - ); - const newElement = document.createElement(action.ChangeTagName); - for (const attr of oldNode.attributes) { - newElement.setAttribute(attr.name, attr.value); - } - for (const childNode of child.node.childNodes) { - newElement.appendChild(childNode); - } + console.log( + "[HOT RELOAD] > ChangeTagName", + child.node, + action.ChangeTagName, + ); + const newElement = document.createElement(action.ChangeTagName); + for (const attr of oldNode.attributes) { + newElement.setAttribute(attr.name, attr.value); + } + for (const childNode of child.node.childNodes) { + newElement.appendChild(childNode); + } - child.node.replaceWith(newElement); - }); + child.node.replaceWith(newElement); } else if (action.RemoveAttribute) { - actions.push(() => { - console.log( - "[HOT RELOAD] > RemoveAttribute", - child.node, - action.RemoveAttribute, - ); - child.node.removeAttribute(action.RemoveAttribute); - }); + console.log( + "[HOT RELOAD] > RemoveAttribute", + child.node, + action.RemoveAttribute, + ); + child.node.removeAttribute(action.RemoveAttribute); } else if (action.SetAttribute) { const [name, value] = action.SetAttribute; - actions.push(() => { - console.log( - "[HOT RELOAD] > SetAttribute", - child.node, - action.SetAttribute, - ); - child.node.setAttribute(name, value); - }); + console.log( + "[HOT RELOAD] > SetAttribute", + child.node, + action.SetAttribute, + ); + child.node.setAttribute(name, value); } else if (action.SetText) { const node = child.node; - actions.push(() => { - console.log("[HOT RELOAD] > SetText", child.node, action.SetText); - node.textContent = action.SetText; - }); + console.log("[HOT RELOAD] > SetText", child.node, action.SetText); + node.textContent = action.SetText; } else if (action.AppendChildren) { - actions.push(() => { - console.log( - "[HOT RELOAD] > AppendChildren", - child.node, - action.AppendChildren, - ); - const newChildren = fromReplacementNode( - action.AppendChildren, - actualChildren, - ); - child.node.append(newChildren); - }); + console.log( + "[HOT RELOAD] > AppendChildren", + child.node, + action.AppendChildren, + ); + const newChildren = fromReplacementNode( + action.AppendChildren, + actualChildren, + ); + child.node.append(newChildren); } else if (action.RemoveChild) { - actions.push(() => { - console.log( - "[HOT RELOAD] > RemoveChild", - child.node, - child.children, - action.RemoveChild, - ); - const toRemove = child.children[action.RemoveChild.at]; - let toRemoveNode = toRemove.node; - if (!toRemoveNode) { - const range = new Range(); - range.setStartBefore(toRemove.start); - range.setEndAfter(toRemove.end); - toRemoveNode = range.deleteContents(); - } else { - toRemoveNode.parentNode.removeChild(toRemoveNode); - } - }); + console.log( + "[HOT RELOAD] > RemoveChild", + child.node, + child.children, + action.RemoveChild, + ); + const toRemove = child.children[action.RemoveChild.at]; + let toRemoveNode = toRemove.node; + if (!toRemoveNode) { + const range = new Range(); + range.setStartBefore(toRemove.start); + range.setEndAfter(toRemove.end); + toRemoveNode = range.deleteContents(); + } else { + toRemoveNode.parentNode.removeChild(toRemoveNode); + } } else if (action.InsertChild) { const newChild = fromReplacementNode( action.InsertChild.child, @@ -159,25 +139,23 @@ function patch(json) { console.warn("InsertChildAfter could not build children."); } const before = children[action.InsertChild.before]; - actions.push(() => { - console.log( - "[HOT RELOAD] > InsertChild", - child, - child.node, - action.InsertChild, - " before ", - before, - ); - if (!before && child.node) { - child.node.appendChild(newChild); - } else { - let node = child.node || child.end.parentElement; - const reference = before - ? before.node || before.start - : child.end; - node.insertBefore(newChild, reference); - } - }); + console.log( + "[HOT RELOAD] > InsertChild", + child, + child.node, + action.InsertChild, + " before ", + before, + ); + if (!before && child.node) { + child.node.appendChild(newChild); + } else { + let node = child.node || child.end.parentElement; + const reference = before + ? before.node || before.start + : child.end; + node.insertBefore(newChild, reference); + } } else if (action.InsertChildAfter) { const newChild = fromReplacementNode( action.InsertChildAfter.child, @@ -196,44 +174,36 @@ function patch(json) { console.warn("InsertChildAfter could not build children."); } const after = children[action.InsertChildAfter.after]; - actions.push(() => { - console.log( - "[HOT RELOAD] > InsertChildAfter", - child, - child.node, - action.InsertChildAfter, - " after ", - after, - ); - if ( - child.node && - (!after || !(after.node || after.start).nextSibling) - ) { - child.node.appendChild(newChild); + console.log( + "[HOT RELOAD] > InsertChildAfter", + child, + child.node, + action.InsertChildAfter, + " after ", + after, + ); + if ( + child.node && + (!after || !(after.node || after.start).nextSibling) + ) { + child.node.appendChild(newChild); + } else { + const node = child.node || child.end; + const parent = + node.nodeType === Node.COMMENT_NODE ? node.parentNode : node; + if (!after) { + parent.appendChild(newChild); } else { - const node = child.node || child.end; - const parent = - node.nodeType === Node.COMMENT_NODE ? node.parentNode : node; - if (!after) { - parent.appendChild(newChild); - } else { - parent.insertBefore( - newChild, - (after.node || after.start).nextSibling, - ); - } + parent.insertBefore( + newChild, + (after.node || after.start).nextSibling, + ); } - }); + } } else { console.warn("[HOT RELOADING] Unmatched action", action); } } - - // actually run the actions - // the reason we delay them is so that children aren't moved before other children are found, etc. - for (const action of actions) { - action(); - } } } } catch (e) { From e89b1389caa184f43f1794b7099b4daee7ef8f92 Mon Sep 17 00:00:00 2001 From: Nesterov Nikita Date: Sat, 19 Jul 2025 08:05:48 +0300 Subject: [PATCH 11/24] fix(hot-reload): rebuild actual children before each patch --- leptos_hot_reload/src/patch.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/leptos_hot_reload/src/patch.js b/leptos_hot_reload/src/patch.js index af49904d9..3e36d8c36 100644 --- a/leptos_hot_reload/src/patch.js +++ b/leptos_hot_reload/src/patch.js @@ -23,12 +23,12 @@ function patch(json) { } for (const [start, end] of instances) { - const actualChildren = childrenFromRange( - start.parentElement, - start, - end, - ); for (const patch of patches) { + const actualChildren = childrenFromRange( + start.parentElement, + start, + end, + ); const child = childAtPath( actualChildren.length > 1 ? { children: actualChildren } From 00e83e0d709bc808ad504cb3fa72b51696b0931d Mon Sep 17 00:00:00 2001 From: Nesterov Nikita Date: Sat, 19 Jul 2025 10:12:58 +0300 Subject: [PATCH 12/24] fix(hot-reload): update InsertChild parent node logic --- leptos_hot_reload/src/patch.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/leptos_hot_reload/src/patch.js b/leptos_hot_reload/src/patch.js index 3e36d8c36..9516d91a8 100644 --- a/leptos_hot_reload/src/patch.js +++ b/leptos_hot_reload/src/patch.js @@ -147,14 +147,15 @@ function patch(json) { " before ", before, ); - if (!before && child.node) { - child.node.appendChild(newChild); + if (!before) { + if (children.length) { + children[children.length - 1].node.after(newChild); + } else { + child.node.appendChild(newChild); + } } else { - let node = child.node || child.end.parentElement; - const reference = before - ? before.node || before.start - : child.end; - node.insertBefore(newChild, reference); + let node = before.node || before.start; + node.parentElement.insertBefore(newChild, node); } } else if (action.InsertChildAfter) { const newChild = fromReplacementNode( From b8d44e20a9ddfb43a3ec16d772c98bcebbd1b6bc Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 19 Jul 2025 09:05:20 +0000 Subject: [PATCH 13/24] [autofix.ci] apply automated fixes --- leptos_hot_reload/src/diff.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/leptos_hot_reload/src/diff.rs b/leptos_hot_reload/src/diff.rs index a12d6d892..e78cdc43b 100644 --- a/leptos_hot_reload/src/diff.rs +++ b/leptos_hot_reload/src/diff.rs @@ -305,7 +305,7 @@ impl LNode { action: PatchAction::InsertChild { before: i, child: new[j - 1] - .to_replacement_node(&old_children), + .to_replacement_node(old_children), }, }); j -= 1; From c98082de7453c3c3c91bc8c9580bfaec605ba304 Mon Sep 17 00:00:00 2001 From: Nesterov Nikita Date: Sat, 19 Jul 2025 13:48:09 +0300 Subject: [PATCH 14/24] fix(hot-reload): insertion before/after fragment in a tag Previous commits that aimed at fixing indexing for Myers algorithm broke insertion before/after a fragment in a html tag, resulting in incorrect ordering/error --- leptos_hot_reload/src/patch.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/leptos_hot_reload/src/patch.js b/leptos_hot_reload/src/patch.js index 9516d91a8..07ba005ba 100644 --- a/leptos_hot_reload/src/patch.js +++ b/leptos_hot_reload/src/patch.js @@ -138,24 +138,24 @@ function patch(json) { } else { console.warn("InsertChildAfter could not build children."); } - const before = children[action.InsertChild.before]; + const beforeNode = children[action.InsertChild.before]; console.log( "[HOT RELOAD] > InsertChild", child, child.node, action.InsertChild, " before ", - before, + beforeNode, ); - if (!before) { - if (children.length) { - children[children.length - 1].node.after(newChild); - } else { - child.node.appendChild(newChild); - } - } else { - let node = before.node || before.start; + if (beforeNode) { + let node = beforeNode.node || beforeNode.start.previousSibling; node.parentElement.insertBefore(newChild, node); + } else if (child.node) { + child.node.appendChild(newChild); + } else if (children) { + let lastNode = children[children.length - 1]; + let afterNode = lastNode.node || lastNode.end.nextSibling; + afterNode.after(newChild); } } else if (action.InsertChildAfter) { const newChild = fromReplacementNode( From 74055a7e1383d19b6e86d12de023a4408b94bfe2 Mon Sep 17 00:00:00 2001 From: Nesterov Nikita Date: Sat, 19 Jul 2025 12:40:53 +0300 Subject: [PATCH 15/24] fix(hot-reload): fix AppendChildren patch command --- leptos_hot_reload/src/patch.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/leptos_hot_reload/src/patch.js b/leptos_hot_reload/src/patch.js index 07ba005ba..41f355db1 100644 --- a/leptos_hot_reload/src/patch.js +++ b/leptos_hot_reload/src/patch.js @@ -99,11 +99,10 @@ function patch(json) { child.node, action.AppendChildren, ); - const newChildren = fromReplacementNode( - action.AppendChildren, - actualChildren, + const newChildren = action.AppendChildren.map((x) => + fromReplacementNode(x, actualChildren), ); - child.node.append(newChildren); + child.node.append(...newChildren); } else if (action.RemoveChild) { console.log( "[HOT RELOAD] > RemoveChild", From 1d7bc021afacc4a9647ed721c6b2d3085d73a196 Mon Sep 17 00:00:00 2001 From: Nesterov Nikita Date: Sat, 19 Jul 2025 15:46:18 +0300 Subject: [PATCH 16/24] fix(hot-reload): ClearChildren couldn't clear fragment view --- leptos_hot_reload/src/patch.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/leptos_hot_reload/src/patch.js b/leptos_hot_reload/src/patch.js index 41f355db1..e31a93d27 100644 --- a/leptos_hot_reload/src/patch.js +++ b/leptos_hot_reload/src/patch.js @@ -38,7 +38,14 @@ function patch(json) { const action = patch.action; if (action == "ClearChildren") { console.log("[HOT RELOAD] > ClearChildren", child.node); - child.node.textContent = ""; + if (child.node) { + child.node.textContent = ""; + } else { + for (const existingChild of child.children) { + let parent = existingChild.node.parentElement; + parent.removeChild(existingChild.node); + } + } } else if (action.ReplaceWith) { console.log( "[HOT RELOAD] > ReplaceWith", From 4a8a212d8405e49c3fb9eb4ad124c69b30c36966 Mon Sep 17 00:00:00 2001 From: Nesterov Nikita Date: Sat, 19 Jul 2025 15:48:57 +0300 Subject: [PATCH 17/24] fix(hot-reload): ReplaceWith couldn't replace Fragment with an Element --- leptos_hot_reload/src/patch.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/leptos_hot_reload/src/patch.js b/leptos_hot_reload/src/patch.js index e31a93d27..f1b6c481b 100644 --- a/leptos_hot_reload/src/patch.js +++ b/leptos_hot_reload/src/patch.js @@ -59,11 +59,17 @@ function patch(json) { if (child.node) { child.node.replaceWith(replacement); } else { - const range = new Range(); - range.setStartAfter(child.start); - range.setEndAfter(child.end); - range.deleteContents(); - child.start.replaceWith(replacement); + if (child.children) { + child.children[0].node.parentElement.insertBefore( + replacement, + child.children[0].node, + ); + for (const existingChild of child.children) { + existingChild.node.parentElement.removeChild( + existingChild.node, + ); + } + } } } else if (action.ChangeTagName) { const oldNode = child.node; From 433f7284e638effa18580f83ac60bda8f40a8c9e Mon Sep 17 00:00:00 2001 From: Nesterov Nikita Date: Sat, 19 Jul 2025 16:25:33 +0300 Subject: [PATCH 18/24] fix(hot-reload): update view map when number of views mismatch --- leptos_hot_reload/src/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/leptos_hot_reload/src/lib.rs b/leptos_hot_reload/src/lib.rs index 388ab0a5e..1f186851b 100644 --- a/leptos_hot_reload/src/lib.rs +++ b/leptos_hot_reload/src/lib.rs @@ -121,6 +121,10 @@ impl ViewMacros { } diffs } else { + // TODO: instead of simply returning no patches, when number of views differs, + // we can compare views content to determine which views were shifted + // or come up with another idea that will allow to send patches when views were shifted/removed/added + lock.insert(path.clone(), new_views); return Ok(None); } } From 777b5e1e54aca2af389db073248f701d81530214 Mon Sep 17 00:00:00 2001 From: martin frances Date: Sun, 20 Jul 2025 02:19:32 +0100 Subject: [PATCH 19/24] chore: examples - bumped version numbers for sqlx and this error. (#4126) --- examples/axum_js_ssr/Cargo.toml | 2 +- examples/errors_axum/Cargo.toml | 2 +- examples/fetch/Cargo.toml | 2 +- examples/regression/Cargo.toml | 2 +- examples/server_fns_axum/Cargo.toml | 2 +- examples/ssr_modes/Cargo.toml | 2 +- examples/ssr_modes_axum/Cargo.toml | 2 +- examples/static_routing/Cargo.toml | 2 +- examples/tailwind_axum/Cargo.toml | 2 +- examples/todo_app_sqlite/Cargo.toml | 6 +++--- examples/todo_app_sqlite_axum/Cargo.toml | 4 ++-- examples/todo_app_sqlite_csr/Cargo.toml | 4 ++-- examples/websocket/Cargo.toml | 2 +- 13 files changed, 17 insertions(+), 17 deletions(-) diff --git a/examples/axum_js_ssr/Cargo.toml b/examples/axum_js_ssr/Cargo.toml index 5d141fc02..162e2da51 100644 --- a/examples/axum_js_ssr/Cargo.toml +++ b/examples/axum_js_ssr/Cargo.toml @@ -19,7 +19,7 @@ leptos_meta = { path = "../../meta" } leptos_axum = { path = "../../integrations/axum", optional = true } leptos_router = { path = "../../router" } serde = { version = "1.0", features = ["derive"] } -thiserror = "1.0" +thiserror = "2.0.12" tokio = { version = "1.39", features = [ "rt-multi-thread", "macros", diff --git a/examples/errors_axum/Cargo.toml b/examples/errors_axum/Cargo.toml index 99ddf1cc3..7e3802e05 100644 --- a/examples/errors_axum/Cargo.toml +++ b/examples/errors_axum/Cargo.toml @@ -18,7 +18,7 @@ tower = { version = "0.4.13", optional = true } tower-http = { version = "0.5.2", features = ["fs"], optional = true } tokio = { version = "1.39", features = ["full"], optional = true } http = { version = "1.1" } -thiserror = "1.0" +thiserror = "2.0.12" wasm-bindgen = "0.2.93" [features] diff --git a/examples/fetch/Cargo.toml b/examples/fetch/Cargo.toml index 1b43e041e..97aa8917d 100644 --- a/examples/fetch/Cargo.toml +++ b/examples/fetch/Cargo.toml @@ -15,7 +15,7 @@ serde = { version = "1.0", features = ["derive"] } log = "0.4.22" console_log = "1.0" console_error_panic_hook = "0.1.7" -thiserror = "1.0" +thiserror = "2.0.12" tracing = "0.1.40" tracing-subscriber = "0.3.18" tracing-subscriber-wasm = "0.1.0" diff --git a/examples/regression/Cargo.toml b/examples/regression/Cargo.toml index 41e8d5578..fb5a79b6c 100644 --- a/examples/regression/Cargo.toml +++ b/examples/regression/Cargo.toml @@ -15,7 +15,7 @@ leptos_meta = { path = "../../meta" } leptos_axum = { path = "../../integrations/axum", optional = true } leptos_router = { path = "../../router" } serde = { version = "1.0", features = ["derive"] } -thiserror = "1.0" +thiserror = "2.0.12" tokio = { version = "1.39", features = [ "rt-multi-thread", "macros", "time" ], optional = true } wasm-bindgen = "0.2.92" diff --git a/examples/server_fns_axum/Cargo.toml b/examples/server_fns_axum/Cargo.toml index e34a4c269..c7a9abb5f 100644 --- a/examples/server_fns_axum/Cargo.toml +++ b/examples/server_fns_axum/Cargo.toml @@ -29,7 +29,7 @@ tower-http = { version = "0.6.2", features = [ "trace", ], optional = true } tokio = { version = "1.39", features = ["full"], optional = true } -thiserror = "2.0.11" +thiserror = "2.0.12" wasm-bindgen = "0.2.93" serde_toml = "0.0.1" toml = "0.8.19" diff --git a/examples/ssr_modes/Cargo.toml b/examples/ssr_modes/Cargo.toml index e063b66c4..8a215a87b 100644 --- a/examples/ssr_modes/Cargo.toml +++ b/examples/ssr_modes/Cargo.toml @@ -17,7 +17,7 @@ leptos_actix = { path = "../../integrations/actix", optional = true } leptos_router = { path = "../../router" } log = "0.4.22" serde = { version = "1.0", features = ["derive"] } -thiserror = "1.0" +thiserror = "2.0.12" tokio = { version = "1.39", features = ["time"] } wasm-bindgen = "0.2.93" diff --git a/examples/ssr_modes_axum/Cargo.toml b/examples/ssr_modes_axum/Cargo.toml index ddcd8e967..a2dc0d516 100644 --- a/examples/ssr_modes_axum/Cargo.toml +++ b/examples/ssr_modes_axum/Cargo.toml @@ -17,7 +17,7 @@ leptos_axum = { path = "../../integrations/axum", optional = true } leptos_router = { path = "../../router" } log = "0.4.22" serde = { version = "1.0", features = ["derive"] } -thiserror = "1.0" +thiserror = "2.0.12" axum = { version = "0.8.1", optional = true } tower = { version = "0.4.13", optional = true } tower-http = { version = "0.5.2", features = ["fs"], optional = true } diff --git a/examples/static_routing/Cargo.toml b/examples/static_routing/Cargo.toml index f3d70fb0a..5d5b01663 100644 --- a/examples/static_routing/Cargo.toml +++ b/examples/static_routing/Cargo.toml @@ -17,7 +17,7 @@ leptos_axum = { path = "../../integrations/axum", optional = true } leptos_router = { path = "../../router" } log = "0.4.22" serde = { version = "1.0", features = ["derive"] } -thiserror = "1.0" +thiserror = "2.0.12" axum = { version = "0.8.1", optional = true } tower = { version = "0.4.13", optional = true } tower-http = { version = "0.5.2", features = ["fs"], optional = true } diff --git a/examples/tailwind_axum/Cargo.toml b/examples/tailwind_axum/Cargo.toml index f84cd6724..4a6a76f81 100644 --- a/examples/tailwind_axum/Cargo.toml +++ b/examples/tailwind_axum/Cargo.toml @@ -20,7 +20,7 @@ tokio = { version = "1.39", features = [ tower = { version = "0.4.13", optional = true } tower-http = { version = "0.5.2", features = ["fs"], optional = true } wasm-bindgen = "0.2.93" -thiserror = "1.0" +thiserror = "2.0.12" tracing = { version = "0.1.40", optional = true } http = "1.1" diff --git a/examples/todo_app_sqlite/Cargo.toml b/examples/todo_app_sqlite/Cargo.toml index ed316639a..5c235f1e6 100644 --- a/examples/todo_app_sqlite/Cargo.toml +++ b/examples/todo_app_sqlite/Cargo.toml @@ -20,7 +20,7 @@ leptos_actix = { path = "../../integrations/actix", optional = true } log = "0.4.22" simple_logger = "5.0" gloo = { git = "https://github.com/rustwasm/gloo" } -sqlx = { version = "0.8.0", features = [ +sqlx = { version = "0.8.6", features = [ "runtime-tokio-rustls", "sqlite", ], optional = true } @@ -44,12 +44,12 @@ denylist = ["actix-files", "actix-web", "leptos_actix", "sqlx"] skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"], []] [package.metadata.leptos] -# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name +# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name output-name = "todo_app_sqlite" # The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup. site-root = "target/site" # The site-root relative folder where all compiled output (JS, WASM and CSS) is written -# Defaults to pkg +# Defaults to pkg site-pkg-dir = "pkg" # [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to //app.css style-file = "./style.css" diff --git a/examples/todo_app_sqlite_axum/Cargo.toml b/examples/todo_app_sqlite_axum/Cargo.toml index e8154b683..70beba257 100644 --- a/examples/todo_app_sqlite_axum/Cargo.toml +++ b/examples/todo_app_sqlite_axum/Cargo.toml @@ -20,11 +20,11 @@ axum = { version = "0.8.1", optional = true } tower = { version = "0.4.13", optional = true } tower-http = { version = "0.5.2", features = ["fs"], optional = true } tokio = { version = "1.39", features = ["full"], optional = true } -sqlx = { version = "0.8.0", features = [ +sqlx = { version = "0.8.6", features = [ "runtime-tokio-rustls", "sqlite", ], optional = true } -thiserror = "1.0" +thiserror = "2.0.12" wasm-bindgen = "0.2.93" [features] diff --git a/examples/todo_app_sqlite_csr/Cargo.toml b/examples/todo_app_sqlite_csr/Cargo.toml index f0e06a795..ba7659314 100644 --- a/examples/todo_app_sqlite_csr/Cargo.toml +++ b/examples/todo_app_sqlite_csr/Cargo.toml @@ -20,11 +20,11 @@ tower = { version = "0.5.1", features = ["util"], optional = true } tower-http = { version = "0.6.1", features = ["fs"], optional = true } tokio = { version = "1.39", features = ["full"], optional = true } http = { version = "1.1" } -sqlx = { version = "0.8.0", features = [ +sqlx = { version = "0.8.6", features = [ "runtime-tokio-rustls", "sqlite", ], optional = true } -thiserror = "2.0" +thiserror = "2.0.12" wasm-bindgen = "0.2.93" [features] diff --git a/examples/websocket/Cargo.toml b/examples/websocket/Cargo.toml index 78e073d2d..cf5b19be5 100644 --- a/examples/websocket/Cargo.toml +++ b/examples/websocket/Cargo.toml @@ -17,7 +17,7 @@ simple_logger = "5.0" serde = { version = "1.0", features = ["derive"] } axum = { version = "0.8.1", optional = true } tokio = { version = "1.39", features = ["full"], optional = true } -thiserror = "2.0" +thiserror = "2.0.12" wasm-bindgen = "0.2.100" [features] From 7f93dd224d0bfad42ed96710e835b2f9e9d57a08 Mon Sep 17 00:00:00 2001 From: Saber Haj Rabiee Date: Sat, 19 Jul 2025 18:38:56 -0700 Subject: [PATCH 20/24] fix(CI): check latest commit for version release instead of version tag (#4150) --- .github/workflows/run-cargo-make-task.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run-cargo-make-task.yml b/.github/workflows/run-cargo-make-task.yml index ad451e6d9..735acf30f 100644 --- a/.github/workflows/run-cargo-make-task.yml +++ b/.github/workflows/run-cargo-make-task.yml @@ -169,7 +169,9 @@ jobs: cd '${{ inputs.directory }}' cargo make --no-workspace --profile=github-actions ci # check the direct-minimal-versions on release - if [[ "${{ github.ref_name }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + COMMIT_MSG=$(git log -1 --pretty=format:'%s') + # Supports: v1.2.3, v1.2.3-alpha, v1.2.3-beta1, v1.2.3-rc.1, etc. + if [[ "$COMMIT_MSG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+(\.?[0-9]+)?)?$ ]]; then cargo make --no-workspace --profile=github-actions check-minimal-versions fi # Check if the counter_isomorphic can be built with leptos_debuginfo cfg flag in release mode From 8c469b85d67da0309491b36ad806a19ad0504136 Mon Sep 17 00:00:00 2001 From: Nesterov Nikita Date: Sun, 20 Jul 2025 07:49:26 +0300 Subject: [PATCH 21/24] fix(hot-reload): ignore clippy::needless_range_loop lint --- leptos_hot_reload/src/diff.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/leptos_hot_reload/src/diff.rs b/leptos_hot_reload/src/diff.rs index e78cdc43b..7d6bac13c 100644 --- a/leptos_hot_reload/src/diff.rs +++ b/leptos_hot_reload/src/diff.rs @@ -254,6 +254,7 @@ impl LNode { let width = old.len() + 1; let height = new.len() + 1; let mut mat = vec![0; width * height]; + #[allow(clippy::needless_range_loop)] for i in 1..width { mat[i] = i; } From 956af8e4667bee1f4df74fe2750595b3b71fb01b Mon Sep 17 00:00:00 2001 From: Dylan Anthony <43723790+dbanty@users.noreply.github.com> Date: Sun, 20 Jul 2025 06:03:49 -0600 Subject: [PATCH 22/24] feat: allow using Actix without default features (#3921) --- integrations/actix/Cargo.toml | 6 ++++-- integrations/actix/src/lib.rs | 13 +++++++++++++ server_fn/Cargo.toml | 5 +++-- server_fn/src/lib.rs | 4 ++-- server_fn/src/middleware/mod.rs | 2 +- server_fn/src/request/mod.rs | 2 +- server_fn/src/response/mod.rs | 2 +- 7 files changed, 25 insertions(+), 9 deletions(-) diff --git a/integrations/actix/Cargo.toml b/integrations/actix/Cargo.toml index 54cb03be0..768faa54f 100644 --- a/integrations/actix/Cargo.toml +++ b/integrations/actix/Cargo.toml @@ -11,7 +11,7 @@ edition.workspace = true [dependencies] actix-http = { workspace = true, default-features = true } actix-files = { workspace = true, default-features = true } -actix-web = { workspace = true, default-features = true } +actix-web = { workspace = true, default-features = false } futures = { workspace = true, default-features = true } any_spawner = { workspace = true, features = ["tokio"] } hydration_context = { workspace = true } @@ -20,7 +20,7 @@ leptos_integration_utils = { workspace = true } leptos_macro = { workspace = true, features = ["actix"] } leptos_meta = { workspace = true, features = ["nonce"] } leptos_router = { workspace = true, features = ["ssr"] } -server_fn = { workspace = true, features = ["actix"] } +server_fn = { workspace = true, features = ["actix-no-default"] } tachys = { workspace = true } serde_json = { workspace = true , default-features = true } parking_lot = { workspace = true, default-features = true } @@ -33,6 +33,8 @@ dashmap = { workspace = true, default-features = true } rustdoc-args = ["--generate-link-to-definition"] [features] +default = ["actix-default"] +actix-default = ["actix-web/default"] islands-router = ["tachys/islands"] tracing = ["dep:tracing"] diff --git a/integrations/actix/src/lib.rs b/integrations/actix/src/lib.rs index 77bbd2e33..aa1fe0ef1 100644 --- a/integrations/actix/src/lib.rs +++ b/integrations/actix/src/lib.rs @@ -282,6 +282,7 @@ pub fn redirect(path: &str) { /// // call ServerFn::register() for each of the server functions you've defined /// } /// +/// # #[cfg(feature = "default")] /// #[actix_web::main] /// async fn main() -> std::io::Result<()> { /// // make sure you actually register your server functions @@ -297,6 +298,8 @@ pub fn redirect(path: &str) { /// .run() /// .await /// } +/// # #[cfg(not(feature = "default"))] +/// # fn main() {} /// ``` /// /// ## Provided Context Types @@ -442,6 +445,7 @@ pub fn handle_server_fns_with_context( /// view! {
"Hello, world!"
} /// } /// +/// # #[cfg(feature = "default")] /// #[actix_web::main] /// async fn main() -> std::io::Result<()> { /// let conf = get_configuration(Some("Cargo.toml")).unwrap(); @@ -461,6 +465,8 @@ pub fn handle_server_fns_with_context( /// .run() /// .await /// } +/// # #[cfg(not(feature = "default"))] +/// # fn main() {} /// ``` /// /// ## Provided Context Types @@ -499,6 +505,7 @@ where /// view! {
"Hello, world!"
} /// } /// +/// # #[cfg(feature = "default")] /// #[actix_web::main] /// async fn main() -> std::io::Result<()> { /// let conf = get_configuration(Some("Cargo.toml")).unwrap(); @@ -521,6 +528,9 @@ where /// .run() /// .await /// } +/// +/// # #[cfg(not(feature = "default"))] +/// # fn main() {} /// ``` /// /// ## Provided Context Types @@ -557,6 +567,7 @@ where /// view! {
"Hello, world!"
} /// } /// +/// # #[cfg(feature = "default")] /// #[actix_web::main] /// async fn main() -> std::io::Result<()> { /// let conf = get_configuration(Some("Cargo.toml")).unwrap(); @@ -576,6 +587,8 @@ where /// .run() /// .await /// } +/// # #[cfg(not(feature = "default"))] +/// # fn main() {} /// ``` /// /// ## Provided Context Types diff --git a/server_fn/Cargo.toml b/server_fn/Cargo.toml index ebefe7bde..0eebe2c7e 100644 --- a/server_fn/Cargo.toml +++ b/server_fn/Cargo.toml @@ -32,7 +32,7 @@ dashmap = { workspace = true, default-features = true } ## servers # actix -actix-web = { optional = true, workspace = true, default-features = true } +actix-web = { optional = true, workspace = true, default-features = false } actix-ws = { optional = true, workspace = true, default-features = true } # axum @@ -108,7 +108,8 @@ axum-no-default = [ "dep:tower-layer", ] form-redirects = [] -actix = ["ssr", "dep:actix-web", "dep:actix-ws", "dep:send_wrapper"] +actix-no-default = ["ssr", "dep:actix-web", "dep:actix-ws", "dep:send_wrapper"] +actix = ["actix-web/default", "actix-no-default"] axum = ["axum/default", "axum-no-default", "axum/ws", "dep:tokio"] browser = [ "dep:gloo-net", diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index b5f0d3b7f..612b254c8 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -120,7 +120,7 @@ pub mod request; /// Types and traits for HTTP responses. pub mod response; -#[cfg(feature = "actix")] +#[cfg(feature = "actix-no-default")] #[doc(hidden)] pub use ::actix_web as actix_export; #[cfg(feature = "axum-no-default")] @@ -1118,7 +1118,7 @@ pub mod axum { } /// Actix integration. -#[cfg(feature = "actix")] +#[cfg(feature = "actix-no-default")] pub mod actix { use crate::{ error::FromServerFnError, middleware::BoxedService, diff --git a/server_fn/src/middleware/mod.rs b/server_fn/src/middleware/mod.rs index dc46fd66e..508ab7634 100644 --- a/server_fn/src/middleware/mod.rs +++ b/server_fn/src/middleware/mod.rs @@ -123,7 +123,7 @@ mod axum { } } -#[cfg(feature = "actix")] +#[cfg(feature = "actix-no-default")] mod actix { use crate::{ error::ServerFnErrorErr, diff --git a/server_fn/src/request/mod.rs b/server_fn/src/request/mod.rs index f10a1b361..9e3fc1875 100644 --- a/server_fn/src/request/mod.rs +++ b/server_fn/src/request/mod.rs @@ -4,7 +4,7 @@ use http::Method; use std::{borrow::Cow, future::Future}; /// Request types for Actix. -#[cfg(feature = "actix")] +#[cfg(feature = "actix-no-default")] pub mod actix; /// Request types for Axum. #[cfg(feature = "axum-no-default")] diff --git a/server_fn/src/response/mod.rs b/server_fn/src/response/mod.rs index 7b88ecd4b..224713c14 100644 --- a/server_fn/src/response/mod.rs +++ b/server_fn/src/response/mod.rs @@ -1,5 +1,5 @@ /// Response types for Actix. -#[cfg(feature = "actix")] +#[cfg(feature = "actix-no-default")] pub mod actix; /// Response types for the browser. #[cfg(feature = "browser")] From 4448b77cde354a9791214cb4b2b89c1a8a9ea7a7 Mon Sep 17 00:00:00 2001 From: mahdi739 <86552031+mahdi739@users.noreply.github.com> Date: Sun, 20 Jul 2025 15:41:17 +0330 Subject: [PATCH 23/24] feat: add `debug_log!`, `debug_error!`, `console_debug_log` and `console_debug_error` (#4160) --- leptos_dom/src/logging.rs | 66 +++++++++++++++++++++++++++++---------- 1 file changed, 50 insertions(+), 16 deletions(-) diff --git a/leptos_dom/src/logging.rs b/leptos_dom/src/logging.rs index b90d11c79..7589fa40f 100644 --- a/leptos_dom/src/logging.rs +++ b/leptos_dom/src/logging.rs @@ -23,6 +23,19 @@ macro_rules! error { ($($t:tt)*) => ($crate::logging::console_error(&format_args!($($t)*).to_string())) } +/// Uses `println!()`-style formatting to log something to the console (in the browser) +/// or via `println!()` (if not in the browser), but only if it's a debug build. +#[macro_export] +macro_rules! debug_log { + ($($x:tt)*) => { + { + if cfg!(debug_assertions) { + $crate::log!($($x)*) + } + } + } +} + /// Uses `println!()`-style formatting to log warnings to the console (in the browser) /// or via `eprintln!()` (if not in the browser), but only if it's a debug build. #[macro_export] @@ -36,6 +49,19 @@ macro_rules! debug_warn { } } +/// Uses `println!()`-style formatting to log errors to the console (in the browser) +/// or via `eprintln!()` (if not in the browser), but only if it's a debug build. +#[macro_export] +macro_rules! debug_error { + ($($x:tt)*) => { + { + if cfg!(debug_assertions) { + $crate::error!($($x)*) + } + } + } +} + const fn log_to_stdout() -> bool { cfg!(not(all( target_arch = "wasm32", @@ -55,7 +81,7 @@ pub fn console_log(s: &str) { } /// Log a warning to the console (in the browser) -/// or via `println!()` (if not in the browser). +/// or via `eprintln!()` (if not in the browser). pub fn console_warn(s: &str) { if log_to_stdout() { eprintln!("{s}"); @@ -65,7 +91,7 @@ pub fn console_warn(s: &str) { } /// Log an error to the console (in the browser) -/// or via `println!()` (if not in the browser). +/// or via `eprintln!()` (if not in the browser). #[inline(always)] pub fn console_error(s: &str) { if log_to_stdout() { @@ -75,21 +101,29 @@ pub fn console_error(s: &str) { } } -/// Log an error to the console (in the browser) +/// Log a string to the console (in the browser) /// or via `println!()` (if not in the browser), but only in a debug build. #[inline(always)] -pub fn console_debug_warn(s: &str) { - #[cfg(debug_assertions)] - { - if log_to_stdout() { - eprintln!("{s}"); - } else { - web_sys::console::warn_1(&JsValue::from_str(s)); - } - } - - #[cfg(not(debug_assertions))] - { - let _ = s; +pub fn console_debug_log(s: &str) { + if cfg!(debug_assertions) { + console_log(s) + } +} + +/// Log a warning to the console (in the browser) +/// or via `eprintln!()` (if not in the browser), but only in a debug build. +#[inline(always)] +pub fn console_debug_warn(s: &str) { + if cfg!(debug_assertions) { + console_warn(s) + } +} + +/// Log an error to the console (in the browser) +/// or via `eprintln!()` (if not in the browser), but only in a debug build. +#[inline(always)] +pub fn console_debug_error(s: &str) { + if cfg!(debug_assertions) { + console_error(s) } } From 152438634686dda8b846e6cda5a2d1270ff363d6 Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Sun, 20 Jul 2025 11:11:29 -0400 Subject: [PATCH 24/24] v0.8.4 --- Cargo.lock | 44 ++++++++++++++++---------------- Cargo.toml | 36 +++++++++++++------------- integrations/axum/Cargo.toml | 2 +- meta/Cargo.toml | 2 +- reactive_graph/Cargo.toml | 2 +- reactive_stores/Cargo.toml | 2 +- reactive_stores_macro/Cargo.toml | 2 +- router/Cargo.toml | 2 +- router_macro/Cargo.toml | 2 +- tachys/Cargo.toml | 2 +- 10 files changed, 48 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 238e7cc9b..f539a6420 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "actix-codec" @@ -1776,7 +1776,7 @@ checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" [[package]] name = "leptos" -version = "0.8.3" +version = "0.8.4" dependencies = [ "any_spawner", "base64", @@ -1830,7 +1830,7 @@ dependencies = [ [[package]] name = "leptos_actix" -version = "0.8.3" +version = "0.8.4" dependencies = [ "actix-files", "actix-http", @@ -1855,7 +1855,7 @@ dependencies = [ [[package]] name = "leptos_axum" -version = "0.8.3" +version = "0.8.4" dependencies = [ "any_spawner", "axum", @@ -1878,7 +1878,7 @@ dependencies = [ [[package]] name = "leptos_config" -version = "0.8.3" +version = "0.8.4" dependencies = [ "config", "regex", @@ -1892,7 +1892,7 @@ dependencies = [ [[package]] name = "leptos_dom" -version = "0.8.3" +version = "0.8.4" dependencies = [ "js-sys", "leptos", @@ -1909,7 +1909,7 @@ dependencies = [ [[package]] name = "leptos_hot_reload" -version = "0.8.3" +version = "0.8.4" dependencies = [ "anyhow", "camino", @@ -1925,7 +1925,7 @@ dependencies = [ [[package]] name = "leptos_integration_utils" -version = "0.8.3" +version = "0.8.4" dependencies = [ "futures", "hydration_context", @@ -1938,7 +1938,7 @@ dependencies = [ [[package]] name = "leptos_macro" -version = "0.8.3" +version = "0.8.4" dependencies = [ "attribute-derive", "cfg-if", @@ -1958,7 +1958,7 @@ dependencies = [ "rustc_version", "serde", "server_fn", - "server_fn_macro 0.8.3", + "server_fn_macro 0.8.4", "syn 2.0.104", "tracing", "trybuild", @@ -1968,7 +1968,7 @@ dependencies = [ [[package]] name = "leptos_meta" -version = "0.8.3" +version = "0.8.4" dependencies = [ "futures", "indexmap", @@ -1982,7 +1982,7 @@ dependencies = [ [[package]] name = "leptos_router" -version = "0.8.3" +version = "0.8.4" dependencies = [ "any_spawner", "either_of", @@ -2006,7 +2006,7 @@ dependencies = [ [[package]] name = "leptos_router_macro" -version = "0.8.3" +version = "0.8.4" dependencies = [ "leptos_macro", "leptos_router", @@ -2018,7 +2018,7 @@ dependencies = [ [[package]] name = "leptos_server" -version = "0.8.3" +version = "0.8.4" dependencies = [ "any_spawner", "base64", @@ -2731,7 +2731,7 @@ dependencies = [ [[package]] name = "reactive_graph" -version = "0.2.3" +version = "0.2.4" dependencies = [ "any_spawner", "async-lock", @@ -2754,7 +2754,7 @@ dependencies = [ [[package]] name = "reactive_stores" -version = "0.2.3" +version = "0.2.4" dependencies = [ "any_spawner", "dashmap", @@ -2773,7 +2773,7 @@ dependencies = [ [[package]] name = "reactive_stores_macro" -version = "0.2.3" +version = "0.2.4" dependencies = [ "convert_case 0.8.0", "proc-macro-error2", @@ -3267,7 +3267,7 @@ dependencies = [ [[package]] name = "server_fn" -version = "0.8.3" +version = "0.8.4" dependencies = [ "actix-web", "actix-ws", @@ -3330,7 +3330,7 @@ dependencies = [ [[package]] name = "server_fn_macro" -version = "0.8.3" +version = "0.8.4" dependencies = [ "const_format", "convert_case 0.8.0", @@ -3343,9 +3343,9 @@ dependencies = [ [[package]] name = "server_fn_macro_default" -version = "0.8.3" +version = "0.8.4" dependencies = [ - "server_fn_macro 0.8.3", + "server_fn_macro 0.8.4", "syn 2.0.104", ] @@ -3536,7 +3536,7 @@ dependencies = [ [[package]] name = "tachys" -version = "0.2.4" +version = "0.2.5" dependencies = [ "any_spawner", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 872b32a7c..0f4db5561 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,7 @@ members = [ exclude = ["benchmarks", "examples", "projects"] [workspace.package] -version = "0.8.3" +version = "0.8.4" edition = "2021" rust-version = "1.88" @@ -51,26 +51,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.3" } -leptos_config = { path = "./leptos_config", version = "0.8.3" } -leptos_dom = { path = "./leptos_dom", version = "0.8.3" } -leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.3" } -leptos_integration_utils = { path = "./integrations/utils", version = "0.8.3" } -leptos_macro = { path = "./leptos_macro", version = "0.8.3" } -leptos_router = { path = "./router", version = "0.8.3" } -leptos_router_macro = { path = "./router_macro", version = "0.8.3" } -leptos_server = { path = "./leptos_server", version = "0.8.3" } -leptos_meta = { path = "./meta", version = "0.8.3" } +leptos = { path = "./leptos", version = "0.8.4" } +leptos_config = { path = "./leptos_config", version = "0.8.4" } +leptos_dom = { path = "./leptos_dom", version = "0.8.4" } +leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.4" } +leptos_integration_utils = { path = "./integrations/utils", version = "0.8.4" } +leptos_macro = { path = "./leptos_macro", version = "0.8.4" } +leptos_router = { path = "./router", version = "0.8.4" } +leptos_router_macro = { path = "./router_macro", version = "0.8.4" } +leptos_server = { path = "./leptos_server", version = "0.8.4" } +leptos_meta = { path = "./meta", version = "0.8.4" } next_tuple = { path = "./next_tuple", version = "0.1.0" } oco_ref = { path = "./oco", version = "0.2.0" } or_poisoned = { path = "./or_poisoned", version = "0.1.0" } -reactive_graph = { path = "./reactive_graph", version = "0.2.3" } -reactive_stores = { path = "./reactive_stores", version = "0.2.3" } -reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.2.3" } -server_fn = { path = "./server_fn", version = "0.8.3" } -server_fn_macro = { path = "./server_fn_macro", version = "0.8.3" } -server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.3" } -tachys = { path = "./tachys", version = "0.2.4" } +reactive_graph = { path = "./reactive_graph", version = "0.2.4" } +reactive_stores = { path = "./reactive_stores", version = "0.2.4" } +reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.2.4" } +server_fn = { path = "./server_fn", version = "0.8.4" } +server_fn_macro = { path = "./server_fn_macro", version = "0.8.4" } +server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.4" } +tachys = { path = "./tachys", version = "0.2.5" } # members deps itertools = { default-features = false, version = "0.14.0" } diff --git a/integrations/axum/Cargo.toml b/integrations/axum/Cargo.toml index 4963652ad..88c351211 100644 --- a/integrations/axum/Cargo.toml +++ b/integrations/axum/Cargo.toml @@ -4,7 +4,7 @@ authors = ["Greg Johnston"] license = "MIT" repository = "https://github.com/leptos-rs/leptos" description = "Axum integrations for the Leptos web framework." -version = "0.8.3" +version = { workspace = true } rust-version.workspace = true edition.workspace = true diff --git a/meta/Cargo.toml b/meta/Cargo.toml index c414f34bc..ad19691a0 100644 --- a/meta/Cargo.toml +++ b/meta/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "leptos_meta" -version = "0.8.3" +version = "0.8.4" 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 5d98b343f..180eef177 100644 --- a/reactive_graph/Cargo.toml +++ b/reactive_graph/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "reactive_graph" -version = "0.2.3" +version = "0.2.4" authors = ["Greg Johnston"] license = "MIT" readme = "../README.md" diff --git a/reactive_stores/Cargo.toml b/reactive_stores/Cargo.toml index f4e30746b..478eaf8b1 100644 --- a/reactive_stores/Cargo.toml +++ b/reactive_stores/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "reactive_stores" -version = "0.2.3" +version = "0.2.4" authors = ["Greg Johnston"] license = "MIT" readme = "../README.md" diff --git a/reactive_stores_macro/Cargo.toml b/reactive_stores_macro/Cargo.toml index 1cb2102e3..31fdfcd2e 100644 --- a/reactive_stores_macro/Cargo.toml +++ b/reactive_stores_macro/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "reactive_stores_macro" -version = "0.2.3" +version = "0.2.4" authors = ["Greg Johnston"] license = "MIT" readme = "../README.md" diff --git a/router/Cargo.toml b/router/Cargo.toml index 119540cc4..36ae86bbc 100644 --- a/router/Cargo.toml +++ b/router/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "leptos_router" -version = "0.8.3" +version = "0.8.4" authors = ["Greg Johnston", "Ben Wishovich"] license = "MIT" readme = "../README.md" diff --git a/router_macro/Cargo.toml b/router_macro/Cargo.toml index 780749208..bac9ce42c 100644 --- a/router_macro/Cargo.toml +++ b/router_macro/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "leptos_router_macro" -version = "0.8.3" +version = "0.8.4" authors = ["Greg Johnston", "Ben Wishovich"] license = "MIT" readme = "../README.md" diff --git a/tachys/Cargo.toml b/tachys/Cargo.toml index 9b8f19d72..ab3e5f575 100644 --- a/tachys/Cargo.toml +++ b/tachys/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tachys" -version = "0.2.4" +version = "0.2.5" authors = ["Greg Johnston"] license = "MIT" readme = "../README.md"