Compare commits

...

73 Commits
4063 ... v0.8.3

Author SHA1 Message Date
Greg Johnston
91fb315fe0 v0.8.3 2025-07-12 20:10:21 -04:00
mskorkowski
6954b77b62 fix: generics on stores (closes #4136) (#4142)
Fixes the case when struct had a generic arguments by adding missing generic arguments into the generated trait and the said trait implementation.
2025-07-12 20:04:48 -04:00
Saber Haj Rabiee
77176f8395 fix(examples): remove redundant cf-worker example (#4140)
Cloudflare has an official template for leptos https://github.com/cloudflare/workers-rs/blob/main/templates/leptos
2025-07-11 10:35:14 -04:00
Greg Johnston
344b79a01b chore: fix cargo-leptos command in README (closes #4134) 2025-07-06 08:51:20 -04:00
Greg Johnston
051059c761 Merge pull request #4115 from leptos-rs/4114-fix
Clean up nested routing ownership and add regression tests
2025-07-01 08:32:52 -04:00
Ryo Hirayama
3c540dd858 Add an example to show server_fn is capable to serve on Cloudflare Workers (#4052)
* Add reqwest-no-ws feature to server_fn

* Add dep:tokio to server_fn/reqwest-no-ws

* Fix

* Refactor reqwest-no-ws feature in server_fn crate for wasm32 support

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* Ad cf-worker example

* Fix error messages for trybuild

* Revert "Fix error messages for trybuild"

This reverts commit 42658dd031.

* Fix CI error by disabling on reqwest-no-ws aslike other feature

* Compact deps and add ci

* Revert all server_fn changes as main

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-06-30 23:44:12 -07:00
Greg Johnston
4125688a0a fix: don't create an extra intermediate outlet (messes with context) 2025-06-30 16:55:19 -04:00
Greg Johnston
bd3b962cfb fix: dispose of all previous owners simultaneously when all routes are complete 2025-06-30 09:51:20 -04:00
Greg Johnston
5dd3c217c4 fix: don't dispose of view owners immediately when outlets rerun 2025-06-30 09:51:02 -04:00
Greg Johnston
ae00e5ae13 test: add regression test for nested context on server 2025-06-30 09:49:16 -04:00
Greg Johnston
1ce671ba08 test: fix signal disposal test 2025-06-30 09:46:22 -04:00
Greg Johnston
ec9f26bd9f chore: remove unused variable 2025-06-30 09:06:18 -04:00
Greg Johnston
831eae31bc fix: much better solution for nested route disposal 2025-06-30 09:05:22 -04:00
Greg Johnston
ff6ae5de25 test: add regression test for signal disposal issue 2025-06-30 08:49:25 -04:00
Greg Johnston
c21712ba04 chore: simplify element_by_id (see #4121) 2025-06-29 17:16:51 -04:00
Greg Johnston
45771b6fd3 fix: correctly rebuild AnyView when the current view doesn't appear in the DOM (closes #4122) 2025-06-29 17:10:32 -04:00
Greg Johnston
f3557970a7 fix: uses EXISTS to mark things that don't exist in the DOM 2025-06-29 17:10:05 -04:00
Greg Johnston
c87ef331b0 fix: fix: correctly construct child links during rebuild 2025-06-29 14:07:48 -04:00
martin frances
e767518142 chore: updated clippy rule affecting stores example (#4120)
status.done().then_some("line-through").unwrap_or_default()
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `if status.done() { "line-through" } else { Default::default() }`
2025-06-28 14:53:27 -04:00
Greg Johnston
f94b681118 fix: correctly clear child route data 2025-06-28 14:31:57 -04:00
Greg Johnston
9c50e49253 test: add regression test for #4088 2025-06-28 14:15:49 -04:00
Greg Johnston
57c7097ede fix: disable InertElement when global class is provided (closes #4116) (#4119) 2025-06-28 13:53:58 -04:00
dependabot[bot]
1a06e0eee8 chore(deps): bump the rust-dependencies group across 1 directory with 18 updates (#4110)
---
updated-dependencies:
- dependency-name: syn
  dependency-version: 2.0.104
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: glib
  dependency-version: 0.20.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: prettyplease
  dependency-version: 0.2.35
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: autocfg
  dependency-version: 1.5.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: derive-where
  dependency-version: 1.5.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: errno
  dependency-version: 0.3.13
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: glib-macros
  dependency-version: 0.20.12
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: libc
  dependency-version: 0.2.174
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: munge
  dependency-version: 0.4.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: munge_macro
  dependency-version: 0.4.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: quinn-udp
  dependency-version: 0.5.13
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: r-efi
  dependency-version: 5.3.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: rustls
  dependency-version: 0.23.28
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: slab
  dependency-version: 0.4.10
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: tracing-attributes
  dependency-version: 0.1.30
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: webpki-roots
  dependency-version: 1.0.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: zerocopy
  dependency-version: 0.8.26
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: zerocopy-derive
  dependency-version: 0.8.26
  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>
2025-06-28 13:53:18 -04:00
bicarlsen
ce9af4a685 fix: use HTML-namespaced InertElement for top-level <svg> elements. (#4109) 2025-06-28 13:53:02 -04:00
martin frances
e0c79eb8d8 chore: bump syn and tokio-tungsenite. (#4117) 2025-06-28 13:52:20 -04:00
Greg Johnston
9fd972971e test: add regression test for back/forward behavior mentioned in #4114 2025-06-27 18:50:28 -04:00
Greg Johnston
9473220639 test: add regression test for #4015 2025-06-27 18:42:30 -04:00
Greg Johnston
ae11812dc6 fix: ensure cleanups run when navigating between sibling Routes in Outlet 2025-06-27 17:59:09 -04:00
Greg Johnston
4c55c25445 chore: clean up unused owner manipulation 2025-06-27 17:59:09 -04:00
Greg Johnston
649b5fbe9e Merge pull request #4114 from metatoaster/regression_examples
A place to put e2e tests for regression, plus reporting issue caused by #4091.
2025-06-27 08:28:20 -04:00
Tommy Yu
adb3e75efc test: e2e that demonstrates failure
- if b37900ec55 from #4091 is reverted the
  failure described in the `pr_4091.feature` will no longer happen.
2025-06-27 22:42:40 +12:00
Tommy Yu
f303aa6d5c test: leptos-rs/leptos#4091 regression 2025-06-27 22:42:40 +12:00
Tommy Yu
73ca3d7b04 Adding a basic regression example 2025-06-27 22:42:40 +12:00
Greg Johnston
235393bfbe chore: remove now-unused join_contexts API (#4113) 2025-06-26 08:58:01 -04:00
Greg Johnston
17d8e2bd09 fix: correctly provide context via nested outlets (closes #4088) (#4091) 2025-06-25 20:00:10 -04:00
Gabriel Lopes Veiga
f51c676e0d feat: add method take for BrowserFormData (#4102) 2025-06-22 13:55:48 -04:00
bicarlsen
cf0aa0e4d7 fix: create svg::InertElement templates in SVG namespace. (#4104) 2025-06-22 13:53:37 -04:00
bicarlsen
df09d4a7f6 fix: update svg::InertElement for dom cache. (#4100)
#4099 introduced caching for inert elements, changing the API for [`dom::Dom::create_element_from_html`](30b0a579ca/tachys/src/renderer/dom.rs (L495)). This updates `svg::InertElement` to match the new API.
2025-06-20 20:39:55 -04:00
TERRORW0LF
30b0a579ca enhance: handle ../ in aria-current for <A/> component (#4051) 2025-06-20 17:09:07 -04:00
dependabot[bot]
50a4c3b0d9 chore(deps): bump autofix-ci/action from 1.3.1 to 1.3.2 (#4072)
Bumps [autofix-ci/action](https://github.com/autofix-ci/action) from 1.3.1 to 1.3.2.
- [Release notes](https://github.com/autofix-ci/action/releases)
- [Commits](https://github.com/autofix-ci/action/compare/v1.3.1...v1.3.2)

---
updated-dependencies:
- dependency-name: autofix-ci/action
  dependency-version: 1.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-20 17:02:54 -04:00
bicarlsen
c76649d77b feat: add inert SVG elements. (#4085) 2025-06-20 17:02:38 -04:00
bicarlsen
911be5007e perf: add template cache for InertElement (#4099) 2025-06-20 17:01:19 -04:00
Greg Johnston
5227221c96 docs: add document on adding class and other attributes to <A/> component (#4086) 2025-06-16 20:00:45 -04:00
Greg Johnston
3f48b77256 feat: impl IntoFragment for AnyView (#4087) 2025-06-16 20:00:32 -04:00
foldedwave
99117f496f fix: correctly remove :capture listeners (closes #4081) (#4082)
Co-authored-by: foldedwave <anon@foldedwave.com>
2025-06-16 20:00:15 -04:00
Moritz Hedtke
cf12ea3404 fix: conflicting changes between #4035 and #4074 (#4090) 2025-06-16 19:59:30 -04:00
martin frances
d555c1e0ce Removed crate once_cell (#4083)
* Removed crate once_cell

As of rust_version 1.80.0 there are now equivalent options in std.

Async and sync changes are as follows.

-use once_cell::sync::Lazy;
+use std::sync::LazyLock;

-use once_cell::sync::Lazy;
+use std::sync::LazyLock;

* ran cargo fmt.

* fixed server_fn errors.

* cargo fmt fixes.

* "use srd::sync" becomes "use std::sync".

* fixed formatting issue.

* formatting issues.

* Fixed error in examples/server_fns_axum

* more formatting issues.

* more formatting issues.

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-06-16 10:38:32 -07:00
Moritz Hedtke
40ea20057f Remove unnecessary Option wrapping (#4035)
* Remove unnecessary Option wrapping.

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-06-16 10:37:26 -07:00
Greg Johnston
5587ccd1eb fix: don't render a comment node for () attributes in template (closes #4079) (#4080) 2025-06-14 15:47:48 -04:00
Greg Johnston
50a9df9eea Merge pull request #4078 from leptos-rs/4066
Fix updates to static class names
2025-06-13 22:49:51 -04:00
Greg Johnston
c46b1c4e25 feat: allow dereferencing LocalResource to an AsyncDerived (see #4063) (#4077) 2025-06-13 18:03:18 -04:00
Greg Johnston
e6f86408a1 fix: ensure that dynamic classes are replaced if they have changed 2025-06-13 18:01:42 -04:00
Greg Johnston
aa13ed9431 fix: ensure that classes are replaced if they have changed (closes #4066) 2025-06-13 18:01:27 -04:00
TERRORW0LF
607a7987e5 fix: unresolved path in server side redirect (closes #4054) (#4074)
* feat: resolve path on server redirect

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-06-13 15:50:18 -04:00
Greg Johnston
c0c3279cbb Merge pull request #4056 from elias098/fixes
Bugfixes to `reactive_stores`
2025-06-13 11:07:09 -04:00
Greg Johnston
ece6d9dd93 feat(CI): add checking minimal-versions on release (#3987)
* feat(CI): add checking minimal-versions

* chore: unify all deps + exact versioning in root workspace for better maintenance

* feat(CI): run minimal-versions only on release tag

* feat(CI): re-adding dependabot with grouped weekly updates
2025-06-13 10:47:53 -04:00
Álvaro Mondéjar Rubio
74ecf4763a docs: update Tauri project to Leptos v0.8.2 (#4020) 2025-06-13 10:47:24 -04:00
Greg Johnston
2c5c69c2fe Merge pull request #4065 from leptos-rs/3729pt2
fix: memory leak introduced by #4015
2025-06-13 10:37:27 -04:00
Tommy Yu
0c275d6540 fix: IntoMaybeErased hygiene on view macro (#4071) 2025-06-13 10:37:13 -04:00
Greg Johnston
6be3266a2e fix: ensure that arena is reestablished for cleanups 2025-06-12 17:13:12 -04:00
Greg Johnston
c3efb8e476 Merge remote-tracking branch 'origin' into 3729pt2 2025-06-11 21:31:38 -04:00
Greg Johnston
32e0551b10 fix: correct set up sandboxing for AsyncDerived futures 2025-06-11 21:31:31 -04:00
Greg Johnston
671ada36ab Merge pull request #4064 from metatoaster/pr_4061
Tests for #4061
2025-06-11 15:49:40 -04:00
Greg Johnston
a9ab4ea372 fix: memory leak introduced by #4015 2025-06-10 21:38:41 -04:00
Tommy Yu
1d72b75d03 test: chained resources as per #4061 2025-06-11 13:32:55 +12:00
Tommy Yu
798d8a4a9e fix: suspense_test corrected due to previous fix
- The `res_overview` previously being tracked inside the async caused an
  extra fetch to happen, and with that tracking being corrected, this no
  longer happens.
- Left a comment about how this is untracked.
2025-06-11 13:32:32 +12:00
elias098
726b7b3116 fix: rework Subfield::track_field to remove duplicate track 2025-06-08 13:56:53 +02:00
elias098
6e91b6fada fix: fix triggers_for_path to prevent duplicate trigger on empty path 2025-06-08 13:40:49 +02:00
elias098
76f1c7a50c fix: triggers_for_path Vec capacity calculation changed to prevent reallocations 2025-06-08 13:19:19 +02:00
Saber Haj Rabiee
8c6059774f feat(CI): re-adding dependabot with grouped weekly updates 2025-06-01 09:20:10 -07:00
Saber Haj Rabiee
0e65034b01 feat(CI): run minimal-versions only on release tag 2025-06-01 09:18:41 -07:00
Saber Haj Rabiee
e1549c5ab3 chore: unify all deps + exact versioning in root workspace for better maintenance 2025-06-01 09:18:40 -07:00
Saber Haj Rabiee
624e91bb2a feat(CI): add checking minimal-versions 2025-06-01 08:55:41 -07:00
106 changed files with 2939 additions and 1238 deletions

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

@@ -0,0 +1,19 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
# Grouping all dependencies in one PR weekly
- package-ecosystem: cargo
directory: "/"
schedule:
interval: weekly
day: monday
open-pull-requests-limit: 1
allow:
- dependency-type: "all"
groups:
rust-dependencies:
patterns:
- "*"

View File

@@ -34,7 +34,7 @@ jobs:
run: cargo fmt --all
- name: Clippy the workspace
run: cargo all-features clippy --allow-dirty --fix --lib --no-deps
- uses: autofix-ci/action@v1.3.1
- uses: autofix-ci/action@v1.3.2
if: ${{ always() }}
with:
fail-fast: false

View File

@@ -72,6 +72,14 @@ jobs:
run: cargo binstall cargo-nextest --no-confirm
- name: Install cargo-all-features
run: cargo install --git https://github.com/sabify/cargo-all-features --branch arbitrary-command-support
# Part of direct-minimal-versions check
- name: Install cargo-hack
if: contains(matrix.toolchain, 'nightly')
uses: taiki-e/install-action@cargo-hack
# Part of direct-minimal-versions check
- name: Install cargo-minimal-versions
if: contains(matrix.toolchain, 'nightly')
uses: taiki-e/install-action@cargo-minimal-versions
- name: Install Trunk
if: contains(inputs.directory, 'examples')
run: cargo binstall trunk --no-confirm
@@ -160,6 +168,10 @@ jobs:
run: |
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
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
- name: ${{ inputs.cargo_make_task }} with --cfg=leptos_debuginfo
if: contains(inputs.directory, 'counter_isomorphic')

517
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -40,9 +40,9 @@ members = [
exclude = ["benchmarks", "examples", "projects"]
[workspace.package]
version = "0.8.2"
version = "0.8.3"
edition = "2021"
rust-version = "1.76"
rust-version = "1.80"
[workspace.dependencies]
# members
@@ -51,70 +51,120 @@ 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.5" }
hydration_context = { path = "./hydration_context", version = "0.3.0" }
leptos = { path = "./leptos", version = "0.8.2" }
leptos_config = { path = "./leptos_config", version = "0.8.2" }
leptos_dom = { path = "./leptos_dom", version = "0.8.2" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.2" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.8.2" }
leptos_macro = { path = "./leptos_macro", version = "0.8.2" }
leptos_router = { path = "./router", version = "0.8.2" }
leptos_router_macro = { path = "./router_macro", version = "0.8.2" }
leptos_server = { path = "./leptos_server", version = "0.8.2" }
leptos_meta = { path = "./meta", version = "0.8.2" }
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" }
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.0" }
reactive_stores = { path = "./reactive_stores", version = "0.2.0" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.2.0" }
server_fn = { path = "./server_fn", version = "0.8.2" }
server_fn_macro = { path = "./server_fn_macro", version = "0.8.2" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.2" }
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.0" }
# common deps
# members deps
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" }
trybuild = { default-features = false, version = "1.0" }
serde_json = { default-features = false, version = "1.0.140" }
trybuild = { default-features = false, version = "1.0.105" }
typed-builder = { default-features = false, version = "0.21.0" }
thiserror = { default-features = false, version = "2.0" }
thiserror = { default-features = false, version = "2.0.12" }
wasm-bindgen = { default-features = false, version = "0.2.100" }
indexmap = { default-features = false, version = "2.9" }
indexmap = { default-features = false, version = "2.9.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" }
rustc-hash = { default-features = false, version = "2.1" }
once_cell = { default-features = false, version = "1.21" }
actix-web = { default-features = false, version = "4.10" }
guardian = { default-features = false, version = "1.3.0" }
rustc-hash = { default-features = false, version = "2.1.1" }
actix-web = { default-features = false, version = "4.11.0" }
tracing = { default-features = false, version = "0.1.41" }
slotmap = { default-features = false, version = "1.0" }
slotmap = { default-features = false, version = "1.0.7" }
futures = { default-features = false, version = "0.3.31" }
dashmap = { default-features = false, version = "6.1" }
dashmap = { default-features = false, version = "6.1.0" }
pin-project-lite = { default-features = false, version = "0.2.16" }
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" }
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" }
tokio = { default-features = false, version = "1.44" }
url = { default-features = false, version = "2.5.4" }
tokio = { default-features = false, version = "1.45.1" }
base64 = { default-features = false, version = "0.22.1" }
cfg-if = { default-features = false, version = "1.0" }
cfg-if = { default-features = false, version = "1.0.0" }
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" }
serde = { default-features = false, version = "1.0" }
parking_lot = { default-features = false, version = "0.12.3" }
axum = { default-features = false, version = "0.8.3" }
proc-macro2 = { default-features = false, version = "1.0.95" }
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" }
syn = { default-features = false, version = "2.0.104" }
xxhash-rust = { default-features = false, version = "0.8.15" }
paste = { default-features = false, version = "1.0" }
quote = { default-features = false, version = "1.0" }
paste = { default-features = false, version = "1.0.15" }
quote = { default-features = false, version = "1.0.40" }
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" }
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" }
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" }
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.35" }
inventory = { default-features = false, version = "0.3.20" }
config = { default-features = false, version = "0.15.11" }
camino = { default-features = false, version = "1.1.9" }
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.10" }
temp-env = { default-features = false, version = "0.3.6" }
uuid = { default-features = false, version = "1.17.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" }
drain_filter_polyfill = { default-features = false, version = "0.1.3" }
tempfile = { default-features = false, version = "3.20.0" }
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" }
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" }
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" }
wasm-bindgen-test = { default-features = false, version = "0.3.50" }
rustversion = { default-features = false, version = "1.0.21" }
getrandom = { default-features = false, version = "0.3.3" }
actix-files = { default-features = false, version = "0.6.6" }
async-lock = { default-features = false, version = "3.4.0" }
[profile.release]
codegen-units = 1

View File

@@ -118,7 +118,7 @@ The `nightly` feature enables the function call syntax for accessing and setting
```bash
cargo install cargo-leptos
cargo leptos new --git https://github.com/leptos-rs/start
cargo leptos new --git https://github.com/leptos-rs/start-axum
cd [your project name]
cargo leptos watch
```

View File

@@ -9,9 +9,9 @@ description = "Spawn asynchronous tasks in an executor-independent way."
edition.workspace = true
[dependencies]
async-executor = { version = "1.13.1", optional = true }
async-executor = { optional = true , workspace = true, default-features = true }
futures = { workspace = true, default-features = true }
glib = { version = "0.20.9", optional = true }
glib = { optional = true , workspace = true, default-features = true }
thiserror = { workspace = true , default-features = true }
tokio = { optional = true, default-features = false, features = [
"rt",
@@ -20,14 +20,14 @@ tracing = { optional = true , workspace = true, default-features = true }
wasm-bindgen-futures = { optional = true , workspace = true, default-features = true }
[dev-dependencies]
futures-lite = { version = "2.6.0", default-features = false }
futures-lite = { default-features = false , workspace = true }
tokio = { default-features = false, features = [
"rt",
"macros",
"time",
] , workspace = true }
wasm-bindgen-test = { version = "0.3.50" }
serial_test = "3.2.0"
wasm-bindgen-test = { workspace = true, default-features = true }
serial_test = { workspace = true, default-features = true }
[features]
async-executor = ["dep:async-executor"]

View File

@@ -0,0 +1,14 @@
[tasks.check-minimal-versions]
condition = { channels = ["nightly"] }
command = "cargo"
args = [
"all-features",
"minimal-versions",
"check",
"--ignore-private",
"--detach-path-deps",
"--direct",
]
install_script = '''
cargo install --git https://github.com/sabify/cargo-all-features --branch arbitrary-command-support
'''

View File

@@ -1,4 +1,8 @@
extend = [{ path = "./lint.toml" }, { path = "./test.toml" }]
extend = [
{ path = "./lint.toml" },
{ path = "./test.toml" },
{ path = "./check-minimal-versions.toml" },
]
[env]
RUSTFLAGS = ""

View File

@@ -23,7 +23,6 @@ leptos = { path = "../../leptos" }
leptos_actix = { path = "../../integrations/actix", optional = true }
leptos_router = { path = "../../router" }
log = "0.4.22"
once_cell = "1.19"
gloo-net = { version = "0.6.0" }
wasm-bindgen = "0.2.93"
serde = { version = "1.0", features = ["derive"] }

View File

@@ -0,0 +1,90 @@
[package]
name = "regression"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
axum = { version = "0.8.1", optional = true }
console_error_panic_hook = "0.1.7"
console_log = "1.0"
leptos = { path = "../../leptos", features = ["tracing"] }
leptos_meta = { path = "../../meta" }
leptos_axum = { path = "../../integrations/axum", optional = true }
leptos_router = { path = "../../router" }
serde = { version = "1.0", features = ["derive"] }
thiserror = "1.0"
tokio = { version = "1.39", features = [ "rt-multi-thread", "macros", "time" ], optional = true }
wasm-bindgen = "0.2.92"
[features]
hydrate = [
"leptos/hydrate",
]
ssr = [
"dep:axum",
"dep:tokio",
"leptos/ssr",
"leptos_meta/ssr",
"dep:leptos_axum",
"leptos_router/ssr",
]
[profile.release]
panic = "abort"
[profile.wasm-release]
inherits = "release"
opt-level = 'z'
lto = true
codegen-units = 1
panic = "abort"
[package.metadata.cargo-all-features]
denylist = ["axum", "tower", "tower-http", "tokio", "sqlx", "leptos_axum"]
skip_feature_sets = [["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "regression"
# 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
site-pkg-dir = "pkg"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "127.0.0.1:3000"
# The port to use for automatic reload monitoring
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
# [Windows] for non-WSL use "npx.cmd playwright test"
# This binary name can be checked in Powershell with Get-Command npx
end2end-cmd = "cargo make test-ui"
end2end-dir = "e2e"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target
#
# Optional. Can be over-ridden with the command line parameter --bin-features
bin-features = ["ssr"]
# If the --no-default-features flag should be used when compiling the bin target
#
# Optional. Defaults to false.
bin-default-features = false
# The features to use when compiling the lib target
#
# Optional. Can be over-ridden with the command line parameter --lib-features
lib-features = ["hydrate"]
# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Leptos
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,8 @@
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/cargo-leptos-webdriver-test.toml" },
]
[env]
CLIENT_PROCESS_NAME = "regression"

View File

@@ -0,0 +1,8 @@
# Regression Tests
This example functions as a catch-all for all current and future regression
test cases that typically happens at integration.
## Quick Start
Run `cargo leptos watch` to run this example.

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,18 @@
[package]
name = "regression_e2e"
version = "0.1.0"
edition = "2021"
[dev-dependencies]
anyhow = "1.0"
async-trait = "0.1.81"
cucumber = "0.21.1"
fantoccini = "0.21.1"
pretty_assertions = "1.4"
serde_json = "1.0"
tokio = { version = "1.39", features = ["macros", "rt-multi-thread", "time"] }
url = "2.5"
[[test]]
name = "app_suite"
harness = false # Allow Cucumber to print output instead of libtest

View File

@@ -0,0 +1,20 @@
extend = { path = "../../cargo-make/main.toml" }
[tasks.test]
env = { RUN_AUTOMATICALLY = false }
condition = { env_true = ["RUN_AUTOMATICALLY"] }
[tasks.ci]
[tasks.test-ui]
command = "cargo"
args = [
"test",
"--test",
"app_suite",
"--",
"--retry",
"2",
"--fail-fast",
"${@}",
]

View File

@@ -0,0 +1,34 @@
# E2E Testing
This example demonstrates e2e testing with Rust using executable requirements.
## Testing Stack
| | Role | Description |
|---|---|---|
| [Cucumber](https://github.com/cucumber-rs/cucumber/tree/main) | Test Runner | Run [Gherkin](https://cucumber.io/docs/gherkin/reference/) specifications as Rust tests |
| [Fantoccini](https://github.com/jonhoo/fantoccini/tree/main) | Browser Client | Interact with web pages through WebDriver |
| [Cargo Leptos](https://github.com/leptos-rs/cargo-leptos) | Build Tool | Compile example and start the server and end-2-end tests |
| [chromedriver](https://chromedriver.chromium.org/downloads) | WebDriver | Provide WebDriver for Chrome |
## Testing Organization
Testing is organized around what a user can do and see/not see. Test scenarios are grouped by the **user action** and the **object** of that action. This makes it easier to locate and reason about requirements.
Here is a brief overview of how things fit together.
```bash
features
└── {action}_{object}.feature # Specify test scenarios
tests
├── fixtures
│ ├── action.rs # Perform a user action (click, type, etc.)
│ ├── check.rs # Assert what a user can see/not see
│ ├── find.rs # Query page elements
│ ├── mod.rs
│ └── world
│ ├── action_steps.rs # Map Gherkin steps to user actions
│ ├── check_steps.rs # Map Gherkin steps to user expectations
│ └── mod.rs
└── app_suite.rs # Test main
```

View File

@@ -0,0 +1,20 @@
@check_issue_4088
Feature: Check that issue 4088 does not reappear
Scenario: I can see the navbar
Given I see the app
And I can access regression test 4088
Then I see the navbar
Scenario: The user info is shared via context
Given I see the app
And I can access regression test 4088
When I select the link Class 1
Then I see the result is the string Assignments for team of user with id 42
Scenario: The user info is shared via context
Given I see the app
And I can access regression test 4088
When I select the link Class 1
When I refresh the browser
Then I see the result is the string Assignments for team of user with id 42

View File

@@ -0,0 +1,8 @@
@check_pr_4015
Feature: Check that PR 4015 does not regress
Scenario: The correct text appears
Given I see the app
And I can access regression test 4015
Then I see the result is the string Some(42)

View File

@@ -0,0 +1,48 @@
@check_pr_4091
Feature: Regression from pull request 4091
Scenario: Signal for testing should work
Given I see the app
And I can access regression test 4091
When I select the link test1
Then I see the result is the string Test1
Scenario: The result returns to empty due to on_cleanup
Given I see the app
And I can access regression test 4091
When I select the following links
| test1 |
| 4091 Home |
Then I see the result is empty
Scenario: The result does not accumulate due to on_cleanup
Given I see the app
And I can access regression test 4091
When I select the following links
| test1 |
| 4091 Home |
| test1 |
| 4091 Home |
Then I see the result is empty
Scenario: I can see the navbar
Given I see the app
And I can access regression test 4091
Then I see the navbar
Scenario: If I navigate to home and back, I can still see the navbar
Given I see the app
And I can access regression test 4091
When I select the following links
| Home |
| 4091 |
Then I see the navbar
Scenario: The signal is not disposed too early
Given I see the app
And I can access regression test 4091
When I select the following links
| test1 |
| Home |
| 4091 |
Then I see the navbar

View File

@@ -0,0 +1,30 @@
mod fixtures;
use anyhow::Result;
use cucumber::World;
use fixtures::world::AppWorld;
use std::{ffi::OsStr, fs::read_dir};
#[tokio::main]
async fn main() -> Result<()> {
// Normally the below is done, but it's now gotten to the point of
// having a sufficient number of tests where the resource contention
// of the concurrently running browsers will cause failures on CI.
// AppWorld::cucumber()
// .fail_on_skipped()
// .run_and_exit("./features")
// .await;
// Mitigate the issue by manually stepping through each feature,
// rather than letting cucumber glob them and dispatch all at once.
for entry in read_dir("./features")? {
let path = entry?.path();
if path.extension() == Some(OsStr::new("feature")) {
AppWorld::cucumber()
.fail_on_skipped()
.run_and_exit(path)
.await;
}
}
Ok(())
}

View File

@@ -0,0 +1,17 @@
use super::{find, world::HOST};
use anyhow::Result;
use fantoccini::Client;
use std::result::Result::Ok;
pub async fn goto_path(client: &Client, path: &str) -> Result<()> {
let url = format!("{}{}", HOST, path);
client.goto(&url).await?;
Ok(())
}
pub async fn click_link(client: &Client, text: &str) -> Result<()> {
let link = find::link_with_text(&client, &text).await?;
link.click().await?;
Ok(())
}

View File

@@ -0,0 +1,20 @@
use crate::fixtures::find;
use anyhow::{Ok, Result};
use fantoccini::Client;
use pretty_assertions::assert_eq;
pub async fn result_text_is(
client: &Client,
expected_text: &str,
) -> Result<()> {
let actual = find::text_at_id(client, "result").await?;
assert_eq!(&actual, expected_text);
Ok(())
}
pub async fn element_exists(client: &Client, id: &str) -> Result<()> {
find::element_by_id(client, id)
.await
.expect(&format!("could not find element with id `{id}`"));
Ok(())
}

View File

@@ -0,0 +1,23 @@
use anyhow::{Ok, Result};
use fantoccini::{elements::Element, Client, Locator};
pub async fn text_at_id(client: &Client, id: &str) -> Result<String> {
let element = element_by_id(client, id)
.await
.expect(format!("no such element with id `{}`", id).as_str());
let text = element.text().await?;
Ok(text)
}
pub async fn link_with_text(client: &Client, text: &str) -> Result<Element> {
let link = client
.wait()
.for_element(Locator::LinkText(text))
.await
.expect(format!("Link not found by `{}`", text).as_str());
Ok(link)
}
pub async fn element_by_id(client: &Client, id: &str) -> Result<Element> {
Ok(client.wait().for_element(Locator::Id(id)).await?)
}

View File

@@ -0,0 +1,4 @@
pub mod action;
pub mod check;
pub mod find;
pub mod world;

View File

@@ -0,0 +1,47 @@
use crate::fixtures::{action, world::AppWorld};
use anyhow::{Ok, Result};
use cucumber::{gherkin::Step, given, when};
#[given("I see the app")]
#[when("I open the app")]
async fn i_open_the_app(world: &mut AppWorld) -> Result<()> {
let client = &world.client;
action::goto_path(client, "").await?;
Ok(())
}
#[given(regex = "^I can access regression test (.*)$")]
#[when(regex = "^I select the link (.*)$")]
async fn i_select_the_link(world: &mut AppWorld, text: String) -> Result<()> {
let client = &world.client;
action::click_link(client, &text).await?;
Ok(())
}
#[given(expr = "I select the following links")]
#[when(expr = "I select the following links")]
async fn i_select_the_following_links(
world: &mut AppWorld,
step: &Step,
) -> Result<()> {
let client = &world.client;
if let Some(table) = step.table.as_ref() {
for row in table.rows.iter() {
action::click_link(client, &row[0]).await?;
}
}
Ok(())
}
#[given(regex = "^I (refresh|reload) the (browser|page)$")]
#[when(regex = "^I (refresh|reload) the (browser|page)$")]
async fn i_refresh_the_browser(world: &mut AppWorld) -> Result<()> {
let client = &world.client;
client.refresh().await?;
Ok(())
}

View File

@@ -0,0 +1,27 @@
use crate::fixtures::{check, world::AppWorld};
use anyhow::{Ok, Result};
use cucumber::then;
#[then(regex = r"^I see the result is empty$")]
async fn i_see_the_result_is_empty(world: &mut AppWorld) -> Result<()> {
let client = &world.client;
check::result_text_is(client, "").await?;
Ok(())
}
#[then(regex = r"^I see the result is the string (.*)$")]
async fn i_see_the_result_is_the_string(
world: &mut AppWorld,
text: String,
) -> Result<()> {
let client = &world.client;
check::result_text_is(client, &text).await?;
Ok(())
}
#[then(regex = r"^I see the navbar$")]
async fn i_see_the_navbar(world: &mut AppWorld) -> Result<()> {
let client = &world.client;
check::element_exists(client, "nav").await?;
Ok(())
}

View File

@@ -0,0 +1,39 @@
pub mod action_steps;
pub mod check_steps;
use anyhow::Result;
use cucumber::World;
use fantoccini::{
error::NewSessionError, wd::Capabilities, Client, ClientBuilder,
};
pub const HOST: &str = "http://127.0.0.1:3000";
#[derive(Debug, World)]
#[world(init = Self::new)]
pub struct AppWorld {
pub client: Client,
}
impl AppWorld {
async fn new() -> Result<Self, anyhow::Error> {
let webdriver_client = build_client().await?;
Ok(Self {
client: webdriver_client,
})
}
}
async fn build_client() -> Result<Client, NewSessionError> {
let mut cap = Capabilities::new();
let arg = serde_json::from_str("{\"args\": [\"-headless\"]}").unwrap();
cap.insert("goog:chromeOptions".to_string(), arg);
let client = ClientBuilder::native()
.capabilities(cap)
.connect("http://localhost:4444")
.await?;
Ok(client)
}

View File

@@ -0,0 +1,65 @@
use crate::{issue_4088::Routes4088, pr_4015::Routes4015, pr_4091::Routes4091};
use leptos::prelude::*;
use leptos_meta::{MetaTags, *};
use leptos_router::{
components::{Route, Router, Routes},
path,
};
pub fn shell(options: LeptosOptions) -> impl IntoView {
view! {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<AutoReload options=options.clone()/>
<HydrationScripts options/>
<MetaTags/>
</head>
<body>
<App/>
</body>
</html>
}
}
#[component]
pub fn App() -> impl IntoView {
provide_meta_context();
let fallback = || view! { "Page not found." }.into_view();
view! {
<Stylesheet id="leptos" href="/pkg/regression.css"/>
<Router>
<main>
<Routes fallback>
<Route path=path!("") view=HomePage/>
<Routes4091/>
<Routes4015/>
<Routes4088/>
</Routes>
</main>
</Router>
}
}
#[server]
async fn server_call() -> Result<(), ServerFnError> {
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
Ok(())
}
#[component]
fn HomePage() -> impl IntoView {
view! {
<Title text="Regression Tests"/>
<h1>"Listing of regression tests"</h1>
<nav>
<ul>
<li><a href="/4091/">"4091"</a></li>
<li><a href="/4015/">"4015"</a></li>
<li><a href="/4088/">"4088"</a></li>
</ul>
</nav>
}
}

View File

@@ -0,0 +1,119 @@
use leptos::{either::Either, prelude::*};
#[allow(unused_imports)]
use leptos_router::{
components::{Outlet, ParentRoute, Redirect, Route},
path, MatchNestedRoutes, NavigateOptions,
};
use serde::{Deserialize, Serialize};
#[component]
pub fn Routes4088() -> impl MatchNestedRoutes + Clone {
view! {
<ParentRoute path=path!("4088") view=|| view!{ <LoggedIn/> }>
<ParentRoute path=path!("") view=||view!{<AssignmentsSelector/>}>
<Route path=path!("/:team_id") view=||view!{<AssignmentsForTeam/>} />
<Route path=path!("") view=||view!{ <p>No class selected</p> }/>
</ParentRoute>
</ParentRoute>
}
.into_inner()
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct UserInfo {
pub id: usize,
}
#[server]
pub async fn get_user_info() -> Result<Option<UserInfo>, ServerFnError> {
Ok(Some(UserInfo { id: 42 }))
}
#[component]
pub fn LoggedIn() -> impl IntoView {
let user_info_resource =
Resource::new(|| (), move |_| async { get_user_info().await });
view! {
<Transition fallback=move || view!{
"loading"
}
>
{move || {
user_info_resource.get()
.map(|a|
match a {
Ok(Some(a)) => Either::Left(view! {
<LoggedInContent user_info={a} />
}),
_ => Either::Right(view!{
<Redirect path="/not_logged_in"/>
})
})
}}
</Transition>
}
}
#[component]
/// Component which provides UserInfo and renders it's child
/// Can also contain some code to check for specific situations (e.g. privacy policies accepted or not? redirect if needed...)
pub fn LoggedInContent(user_info: UserInfo) -> impl IntoView {
provide_context(user_info.clone());
if user_info.id == 42 {
Either::Left(Outlet())
} else {
Either::Right(
view! { <Redirect path="/somewhere" options={NavigateOptions::default()}/> },
)
}
}
#[component]
/// This component also uses Outlet (so nested Outlet)
fn AssignmentsSelector() -> impl IntoView {
let user_info = use_context::<UserInfo>().expect("user info not provided");
view! {
<p>"Assignments for user with ID: "{user_info.id}</p>
<ul id="nav">
<li><a href="/4088/1">"Class 1"</a></li>
<li><a href="/4088/2">"Class 2"</a></li>
<li><a href="/4088/3">"Class 3"</a></li>
</ul>
<Outlet />
}
}
#[component]
fn AssignmentsForTeam() -> impl IntoView {
// THIS FAILS -> Because of the nested outlet in LoggedInContent > AssignmentsSelector?
// It did not fail when LoggedIn did not use a resource and transition (but a hardcoded UserInfo in the component)
let user_info = use_context::<UserInfo>().expect("user info not provided");
let items = vec!["Assignment 1", "Assignment 2", "Assignment 3"];
view! {
<p id="result">"Assignments for team of user with id " {user_info.id}</p>
<ul>
{
items.into_iter().map(|item| {
view! {
<Assignment name=item.to_string() />
}
}).collect_view()
}
</ul>
}
}
#[component]
fn Assignment(name: String) -> impl IntoView {
let user_info = use_context::<UserInfo>().expect("user info not provided");
view! {
<li>{name}" "{user_info.id}</li>
}
}

View File

@@ -0,0 +1,12 @@
pub mod app;
mod issue_4088;
mod pr_4015;
mod pr_4091;
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
use app::*;
console_error_panic_hook::set_once();
leptos::mount::hydrate_body(App);
}

View File

@@ -0,0 +1,37 @@
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use axum::Router;
use leptos::prelude::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use regression::app::{shell, App};
let conf = get_configuration(None).unwrap();
let addr = conf.leptos_options.site_addr;
let leptos_options = conf.leptos_options;
// Generate the list of routes in your Leptos App
let routes = generate_route_list(App);
let app = Router::new()
.leptos_routes(&leptos_options, routes, {
let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone())
})
.fallback(leptos_axum::file_and_error_handler(shell))
.with_state(leptos_options);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
println!("listening on http://{}", &addr);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}
#[cfg(not(feature = "ssr"))]
pub fn main() {
// no client-side main function
// unless we want this to work with e.g., Trunk for pure client-side testing
// see lib.rs for hydration function instead
}

View File

@@ -0,0 +1,29 @@
use leptos::{context::Provider, prelude::*};
use leptos_router::{
components::{ParentRoute, Route},
nested_router::Outlet,
path,
};
#[component]
pub fn Routes4015() -> impl leptos_router::MatchNestedRoutes + Clone {
view! {
<ParentRoute path=path!("4015") view=|| view! {
<Provider value=42i32>
<Outlet/>
</Provider>
}>
<Route path=path!("") view=Child/>
</ParentRoute>
}
.into_inner()
}
#[component]
fn Child() -> impl IntoView {
let value = use_context::<i32>();
view! {
<p id="result">{format!("{value:?}")}</p>
}
}

View File

@@ -0,0 +1,68 @@
use leptos::{context::Provider, prelude::*};
use leptos_router::{
components::{ParentRoute, Route, A},
nested_router::Outlet,
path,
};
// FIXME This should be a set rather than a naive vec for push and pop, as
// it may be possible for unexpected token be popped/pushed on multi-level
// navigation. For basic naive tests it should be Fine(TM).
#[derive(Clone)]
struct Expectations(Vec<&'static str>);
#[component]
pub fn Routes4091() -> impl leptos_router::MatchNestedRoutes + Clone {
view! {
<ParentRoute path=path!("4091") view=Container>
<Route path=path!("") view=Root/>
<Route path=path!("test1") view=Test1/>
</ParentRoute>
}
.into_inner()
}
#[component]
fn Container() -> impl IntoView {
let rw_signal = RwSignal::new(Expectations(Vec::new()));
provide_context(rw_signal);
view! {
<nav id="nav">
<ul>
<li><A href="/">"Home"</A></li>
<li><A href="./">"4091 Home"</A></li>
<li><A href="test1">"test1"</A></li>
</ul>
</nav>
<div id="result">{move || {
rw_signal.with(|ex| ex.0.iter().fold(String::new(), |a, b| a + b + " "))
}}</div>
<Provider value=rw_signal>
<Outlet/>
</Provider>
}
}
#[component]
fn Root() -> impl IntoView {
view! {
<div>"This is Root"</div>
}
}
#[component]
fn Test1() -> impl IntoView {
let signal = expect_context::<RwSignal<Expectations>>();
on_cleanup(move || {
signal.update(|ex| {
ex.0.pop();
});
});
view! {
{move || signal.update(|ex| ex.0.push("Test1"))}
<div>"This is Test1"</div>
}
}

View File

@@ -0,0 +1,3 @@
body {
font-family: sans-serif;
}

View File

@@ -38,7 +38,6 @@ strum = { version = "0.27.1", features = ["strum_macros", "derive"] }
notify = { version = "8.0", optional = true }
pin-project-lite = "0.2.14"
dashmap = { version = "6.0", optional = true }
once_cell = { version = "1.19", optional = true }
async-broadcast = { version = "0.7.1", optional = true }
bytecheck = "0.8.0"
rkyv = { version = "0.8.8" }
@@ -54,7 +53,6 @@ ssr = [
"dep:leptos_axum",
"dep:notify",
"dep:dashmap",
"dep:once_cell",
"dep:async-broadcast",
]

View File

@@ -424,7 +424,7 @@ pub fn FileUploadWithProgress() -> impl IntoView {
use async_broadcast::{broadcast, Receiver, Sender};
use dashmap::DashMap;
use futures::Stream;
use once_cell::sync::Lazy;
use std::sync::LazyLock;
struct File {
total: usize,
@@ -432,7 +432,8 @@ pub fn FileUploadWithProgress() -> impl IntoView {
rx: Receiver<usize>,
}
static FILES: Lazy<DashMap<String, File>> = Lazy::new(DashMap::new);
static FILES: LazyLock<DashMap<String, File>> =
LazyLock::new(DashMap::new);
pub async fn add_chunk(filename: &str, len: usize) {
println!("[{filename}]\tadding {len}");

View File

@@ -159,7 +159,7 @@ fn TodoRow(
view! {
<li style:text-decoration=move || {
status.done().then_some("line-through").unwrap_or_default()
if status.done() { "line-through" } else { Default::default() }
}>
<p

View File

@@ -52,7 +52,7 @@ Feature: Using instrumented counters to test regression from #3502.
| list_items | 1 |
| get_item | 1 |
| inspect_item_root | 0 |
| inspect_item_field | 4 |
| inspect_item_field | 3 |
Scenario: Follow paths ordinarily down to a target
Given I select the following links

View File

@@ -477,6 +477,8 @@ fn ItemInspect() -> impl IntoView {
move || params.get().map(|p| p.path),
move |p| async move {
// leptos::logging::log!("res_inspect: res_overview.await");
// Note: this resource is untracked here, though `params` changing
// will nonetheless results in the "expected" tracked updates.
let overview = res_overview.await;
// leptos::logging::log!("res_inspect: resolved res_overview.await");
// let result =

View File

@@ -16,7 +16,6 @@ futures = { workspace = true, default-features = true }
serde = { features = ["derive"] , workspace = true, default-features = true }
wasm-bindgen = { workspace = true, optional = true , default-features = true }
js-sys = { optional = true , workspace = true, default-features = true }
once_cell = { workspace = true, default-features = true }
pin-project-lite = { workspace = true, default-features = true }
[features]

View File

@@ -7,10 +7,12 @@ use super::{SerializedDataId, SharedContext};
use crate::{PinnedFuture, PinnedStream};
use core::fmt::Debug;
use js_sys::Array;
use once_cell::sync::Lazy;
use std::{
fmt::Display,
sync::atomic::{AtomicBool, AtomicUsize, Ordering},
sync::{
atomic::{AtomicBool, AtomicUsize, Ordering},
LazyLock,
},
};
use throw_error::{Error, ErrorId};
use wasm_bindgen::{prelude::wasm_bindgen, JsCast};
@@ -79,8 +81,8 @@ pub struct HydrateSharedContext {
id: AtomicUsize,
is_hydrating: AtomicBool,
during_hydration: AtomicBool,
errors: Lazy<Vec<(SerializedDataId, ErrorId, Error)>>,
incomplete: Lazy<Vec<SerializedDataId>>,
errors: LazyLock<Vec<(SerializedDataId, ErrorId, Error)>>,
incomplete: LazyLock<Vec<SerializedDataId>>,
}
impl HydrateSharedContext {
@@ -90,8 +92,8 @@ impl HydrateSharedContext {
id: AtomicUsize::new(0),
is_hydrating: AtomicBool::new(true),
during_hydration: AtomicBool::new(true),
errors: Lazy::new(serialized_errors),
incomplete: Lazy::new(incomplete_chunks),
errors: LazyLock::new(serialized_errors),
incomplete: LazyLock::new(incomplete_chunks),
}
}
@@ -104,8 +106,8 @@ impl HydrateSharedContext {
id: AtomicUsize::new(0),
is_hydrating: AtomicBool::new(false),
during_hydration: AtomicBool::new(true),
errors: Lazy::new(serialized_errors),
incomplete: Lazy::new(incomplete_chunks),
errors: LazyLock::new(serialized_errors),
incomplete: LazyLock::new(incomplete_chunks),
}
}
}

View File

@@ -9,8 +9,8 @@ rust-version.workspace = true
edition.workspace = true
[dependencies]
actix-http = "3.10"
actix-files = "0.6"
actix-http = { workspace = true, default-features = true }
actix-files = { workspace = true, default-features = true }
actix-web = { workspace = true, default-features = true }
futures = { workspace = true, default-features = true }
any_spawner = { workspace = true, features = ["tokio"] }
@@ -28,7 +28,6 @@ tracing = { optional = true , workspace = true, default-features = true }
tokio = { features = ["rt", "fs"] , workspace = true, default-features = true }
send_wrapper = { workspace = true, default-features = true }
dashmap = { workspace = true, default-features = true }
once_cell = { workspace = true, default-features = true }
[package.metadata.docs.rs]
rustdoc-args = ["--generate-link-to-definition"]

View File

@@ -38,7 +38,6 @@ use leptos_router::{
static_routes::{RegenerationFn, ResolvedStaticPath},
ExpandOptionals, Method, PathSegment, RouteList, RouteListing, SsrMode,
};
use once_cell::sync::Lazy;
use parking_lot::RwLock;
use send_wrapper::SendWrapper;
use server_fn::{
@@ -51,7 +50,7 @@ use std::{
future::Future,
ops::{Deref, DerefMut},
path::Path,
sync::Arc,
sync::{Arc, LazyLock},
};
/// This struct lets you define headers and override the status of the Response from an Element or a Server Function
@@ -1210,8 +1209,8 @@ impl StaticRouteGenerator {
}
}
static STATIC_HEADERS: Lazy<DashMap<String, ResponseOptions>> =
Lazy::new(DashMap::new);
static STATIC_HEADERS: LazyLock<DashMap<String, ResponseOptions>> =
LazyLock::new(DashMap::new);
fn was_404(owner: &Owner) -> bool {
let resp = owner.with(|| expect_context::<ResponseOptions>());

View File

@@ -4,7 +4,7 @@ authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
description = "Axum integrations for the Leptos web framework."
version = "0.8.2"
version = "0.8.3"
rust-version.workspace = true
edition.workspace = true
@@ -13,7 +13,7 @@ any_spawner = { workspace = true, features = ["tokio"] }
hydration_context = { workspace = true }
axum = { default-features = false, features = [
"matched-path",
] , workspace = true }
], workspace = true }
dashmap = { workspace = true, default-features = true }
futures = { workspace = true, default-features = true }
leptos = { workspace = true, features = ["nonce", "ssr"] }
@@ -23,16 +23,18 @@ leptos_meta = { workspace = true, features = ["ssr", "nonce"] }
leptos_router = { workspace = true, features = ["ssr"] }
leptos_integration_utils = { workspace = true }
tachys = { workspace = true }
once_cell = { workspace = true, default-features = true }
parking_lot = { workspace = true, default-features = true }
tokio = { default-features = false , workspace = true }
tower = { features = ["util"] , workspace = true, default-features = true }
tower-http = "0.6.2"
tracing = { optional = true , workspace = true, default-features = true }
tokio = { default-features = false, workspace = true }
tower = { features = ["util"], workspace = true, default-features = true }
tower-http = { workspace = true, default-features = true }
tracing = { optional = true, workspace = true, default-features = true }
[dev-dependencies]
axum = { workspace = true, default-features = true }
tokio = { features = ["net", "rt-multi-thread"] , workspace = true, default-features = true }
tokio = { features = [
"net",
"rt-multi-thread",
], workspace = true, default-features = true }
[features]
wasm = []

View File

@@ -69,12 +69,12 @@ use leptos_router::{
static_routes::RegenerationFn, ExpandOptionals, PathSegment, RouteList,
RouteListing, SsrMode,
};
#[cfg(feature = "default")]
use once_cell::sync::Lazy;
use parking_lot::RwLock;
use server_fn::{error::ServerFnErrorErr, redirect::REDIRECT_HEADER};
#[cfg(feature = "default")]
use std::path::Path;
#[cfg(feature = "default")]
use std::sync::LazyLock;
use std::{collections::HashSet, fmt::Debug, io, pin::Pin, sync::Arc};
#[cfg(feature = "default")]
use tower::util::ServiceExt;
@@ -1522,8 +1522,8 @@ impl StaticRouteGenerator {
}
#[cfg(feature = "default")]
static STATIC_HEADERS: Lazy<DashMap<String, ResponseOptions>> =
Lazy::new(DashMap::new);
static STATIC_HEADERS: LazyLock<DashMap<String, ResponseOptions>> =
LazyLock::new(DashMap::new);
#[cfg(feature = "default")]
fn was_404(owner: &Owner) -> bool {

View File

@@ -24,14 +24,14 @@ leptos_hot_reload = { workspace = true }
leptos_macro = { workspace = true }
leptos_server = { workspace = true, features = ["tachys"] }
leptos_config = { workspace = true }
leptos-spin-macro = { version = "0.2.0", optional = true }
leptos-spin-macro = { optional = true , workspace = true, default-features = true }
oco_ref = { workspace = true }
or_poisoned = { workspace = true }
paste = { workspace = true, default-features = true }
rand = { version = "0.9.1", optional = true }
rand = { optional = true , workspace = true, default-features = true }
# NOTE: While not used directly, `getrandom`'s `wasm_js` feature is needed when `rand` is used on WASM to
# avoid a compilation error
getrandom = { version = "0.3.3", optional = true }
getrandom = { optional = true , workspace = true, default-features = true }
reactive_graph = { workspace = true, features = ["serde"] }
rustc-hash = { workspace = true, default-features = true }
tachys = { workspace = true, features = [
@@ -42,7 +42,7 @@ tachys = { workspace = true, features = [
thiserror = { workspace = true, default-features = true }
tracing = { optional = true, workspace = true, default-features = true }
typed-builder = { workspace = true, default-features = true }
typed-builder-macro = "0.21.0"
typed-builder-macro = { workspace = true, default-features = true }
serde = { workspace = true, default-features = true }
serde_json = { optional = true, workspace = true, default-features = true }
server_fn = { workspace = true, features = ["form-redirects", "browser"] }
@@ -101,6 +101,11 @@ trace-component-props = [
delegation = ["tachys/delegation"]
islands-router = ["tachys/mark_branches"]
[dev-dependencies]
tokio = { features = ["rt-multi-thread", "macros"] , workspace = true, default-features = true }
tokio-test = { workspace = true, default-features = true }
any_spawner = { workspace = true, features = ["futures-executor", "tokio"] }
[build-dependencies]
rustc_version = { workspace = true, default-features = true }

View File

@@ -90,6 +90,7 @@ impl<T: RenderHtml> RenderHtml for View<T> {
type Owned = View<T::Owned>;
const MIN_LENGTH: usize = <T as RenderHtml>::MIN_LENGTH;
const EXISTS: bool = <T as RenderHtml>::EXISTS;
async fn resolve(self) -> Self::AsyncOutput {
self.inner.resolve().await

79
leptos/tests/pr_4061.rs Normal file
View File

@@ -0,0 +1,79 @@
#[cfg(feature = "ssr")]
mod imports {
pub use any_spawner::Executor;
pub use futures::StreamExt;
pub use leptos::prelude::*;
}
#[cfg(feature = "ssr")]
#[tokio::test]
async fn chain_await_resource() {
use imports::*;
_ = Executor::init_tokio();
let owner = Owner::new();
owner.set();
let (rs, ws) = signal(0);
let source = Resource::new(
|| (),
move |_| async move {
#[cfg(feature = "ssr")]
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
1
},
);
let consuming = Resource::new(
|| (),
move |_| async move {
let result = source.await;
ws.update(|s| *s += 1);
result
},
);
let app = view! {
<Suspense>{
move || {
Suspend::new(async move {
consuming.await;
rs.get()
})
}
}</Suspense>
};
assert_eq!(app.to_html_stream_in_order().collect::<String>().await, "1");
}
#[cfg(feature = "ssr")]
#[tokio::test]
async fn chain_no_await_resource() {
use imports::*;
_ = Executor::init_tokio();
let owner = Owner::new();
owner.set();
let (rs, ws) = signal(0);
let source = Resource::new(|| (), move |_| async move { 1 });
let consuming = Resource::new(
|| (),
move |_| async move {
let result = source.await;
ws.update(|s| *s += 1);
result
},
);
let app = view! {
<Suspense>{
move || {
Suspend::new(async move {
consuming.await;
rs.get()
})
}
}</Suspense>
};
assert_eq!(app.to_html_stream_in_order().collect::<String>().await, "1");
}

View File

@@ -10,19 +10,19 @@ rust-version.workspace = true
edition.workspace = true
[dependencies]
config = { version = "0.15.11", default-features = false, features = [
config = { default-features = false, features = [
"toml",
"convert-case",
] }
regex = "1.11"
] , workspace = true }
regex = { workspace = true, default-features = true }
serde = { features = ["derive", "rc"] , workspace = true, default-features = true }
thiserror = { workspace = true , default-features = true }
typed-builder = { workspace = true , default-features = true }
[dev-dependencies]
tokio = { features = ["rt", "macros"] , workspace = true, default-features = true }
tempfile = "3.19"
temp-env = { version = "0.3.6", features = ["async_closure"] }
tempfile = { workspace = true, default-features = true }
temp-env = { features = ["async_closure"] , workspace = true, default-features = true }
[package.metadata.docs.rs]
rustdoc-args = ["--generate-link-to-definition"]

View File

@@ -10,7 +10,7 @@ rust-version.workspace = true
edition.workspace = true
[dependencies]
anyhow = "1.0"
anyhow = { workspace = true, default-features = true }
serde = { features = ["derive"] , workspace = true, default-features = true }
syn = { features = [
"full",
@@ -23,6 +23,6 @@ quote = { workspace = true, default-features = true }
rstml = { workspace = true, default-features = true }
proc-macro2 = { features = ["span-locations", "nightly"] , workspace = true, default-features = true }
parking_lot = { workspace = true, default-features = true }
walkdir = "2.5"
camino = "1.1"
walkdir = { workspace = true, default-features = true }
camino = { workspace = true, default-features = true }
indexmap = { workspace = true, default-features = true }

View File

@@ -13,11 +13,11 @@ edition.workspace = true
proc-macro = true
[dependencies]
attribute-derive = { version = "0.10.3", features = ["syn-full"] }
attribute-derive = { features = ["syn-full"] , workspace = true, default-features = true }
cfg-if = { workspace = true, default-features = true }
html-escape = { workspace = true, default-features = true }
itertools = { workspace = true , default-features = true }
prettyplease = "0.2.32"
prettyplease = { workspace = true, default-features = true }
proc-macro-error2 = { default-features = false , workspace = true }
proc-macro2 = { workspace = true, default-features = true }
quote = { workspace = true, default-features = true }
@@ -26,17 +26,17 @@ rstml = { workspace = true, default-features = true }
leptos_hot_reload = { workspace = true }
server_fn_macro = { workspace = true }
convert_case = { workspace = true , default-features = true }
uuid = { version = "1.16", features = ["v4"] }
uuid = { features = ["v4"] , workspace = true, default-features = true }
tracing = { optional = true , workspace = true, default-features = true }
[dev-dependencies]
log = "0.4.27"
log = { workspace = true, default-features = true }
typed-builder = { workspace = true, default-features = true }
trybuild = { workspace = true , default-features = true }
leptos = { path = "../leptos" }
leptos_router = { path = "../router", features = ["ssr"] }
server_fn = { path = "../server_fn", features = ["cbor"] }
insta = "1.42"
insta = { workspace = true, default-features = true }
serde = { workspace = true, default-features = true }
[build-dependencies]

View File

@@ -44,6 +44,8 @@ pub fn render_view(
view_marker: Option<String>,
disable_inert_html: bool,
) -> Option<TokenStream> {
let disable_inert_html = disable_inert_html || global_class.is_some();
let (base, should_add_view) = match nodes.len() {
0 => {
let span = Span::call_site();
@@ -112,9 +114,9 @@ fn is_inert_element(orig_node: &Node<impl CustomNode>) -> bool {
return false;
}
// also doesn't work if the top-level element is an SVG/MathML element
// also doesn't work if the top-level element is a MathML element
let el_name = el.name().to_string();
if is_svg_element(&el_name) || is_math_ml_element(&el_name) {
if is_math_ml_element(&el_name) {
return false;
}
}
@@ -300,7 +302,7 @@ fn inert_element_to_tokens(
node: &Node<impl CustomNode>,
escape_text: bool,
global_class: Option<&TokenTree>,
) -> Option<TokenStream> {
) -> TokenStream {
let mut html = InertElementBuilder::new(global_class);
let mut nodes = VecDeque::from([Item::Node(node, escape_text)]);
@@ -396,9 +398,117 @@ fn inert_element_to_tokens(
html.finish();
Some(quote! {
quote! {
::leptos::tachys::html::InertElement::new(#html)
})
}
}
/// # Note
/// Should not be used on top level `<svg>` elements.
/// Use [`inert_element_to_tokens`] instead.
fn inert_svg_element_to_tokens(
node: &Node<impl CustomNode>,
escape_text: bool,
global_class: Option<&TokenTree>,
) -> TokenStream {
let mut html = InertElementBuilder::new(global_class);
let mut nodes = VecDeque::from([Item::Node(node, escape_text)]);
while let Some(current) = nodes.pop_front() {
match current {
Item::ClosingTag(tag) => {
// closing tag
html.push_str("</");
html.push_str(&tag);
html.push('>');
}
Item::Node(current, escape) => {
match current {
Node::RawText(raw) => {
let text = raw.to_string_best();
let text = if escape {
html_escape::encode_text(&text)
} else {
text.into()
};
html.push_str(&text);
}
Node::Text(text) => {
let text = text.value_string();
let text = if escape {
html_escape::encode_text(&text)
} else {
text.into()
};
html.push_str(&text);
}
Node::Element(node) => {
let self_closing = is_self_closing(node);
let el_name = node.name().to_string();
let escape = el_name != "script"
&& el_name != "style"
&& el_name != "textarea";
// opening tag
html.push('<');
html.push_str(&el_name);
for attr in node.attributes() {
if let NodeAttribute::Attribute(attr) = attr {
let attr_name = attr.key.to_string();
// trim r# from raw identifiers like r#as
let attr_name =
attr_name.trim_start_matches("r#");
if attr_name != "class" {
html.push(' ');
html.push_str(attr_name);
}
if let Some(value) =
attr.possible_value.to_value()
{
if let KVAttributeValue::Expr(Expr::Lit(
lit,
)) = &value.value
{
if let Lit::Str(txt) = &lit.lit {
let value = txt.value();
let value = html_escape::encode_double_quoted_attribute(&value);
if attr_name == "class" {
html.push_class(&value);
} else {
html.push_str("=\"");
html.push_str(&value);
html.push('"');
}
}
}
};
}
}
html.push('>');
// render all children
if !self_closing {
nodes.push_front(Item::ClosingTag(el_name));
let children = node.children.iter().rev();
for child in children {
nodes.push_front(Item::Node(child, escape));
}
}
}
_ => {}
}
}
}
}
html.finish();
quote! {
::leptos::tachys::svg::InertElement::new(#html)
}
}
fn element_children_to_tokens(
@@ -431,7 +541,9 @@ fn element_children_to_tokens(
} else if cfg!(feature = "__internal_erase_components") {
Some(quote! {
.child(
leptos::tachys::view::iterators::StaticVec::from(vec![#(#children.into_maybe_erased()),*])
::leptos::tachys::view::iterators::StaticVec::from(vec![#(
::leptos::prelude::IntoMaybeErased::into_maybe_erased(#children)
),*])
)
})
} else if children.len() > 16 {
@@ -481,7 +593,9 @@ fn fragment_to_tokens(
children.into_iter().next()
} else if cfg!(feature = "__internal_erase_components") {
Some(quote! {
leptos::tachys::view::iterators::StaticVec::from(vec![#(#children.into_maybe_erased()),*])
::leptos::tachys::view::iterators::StaticVec::from(vec![#(
::leptos::prelude::IntoMaybeErased::into_maybe_erased(#children)
),*])
})
} else if children.len() > 16 {
// implementations of various traits used in routing and rendering are implemented for
@@ -593,7 +707,17 @@ fn node_to_tokens(
let escape = el_name != "script"
&& el_name != "style"
&& el_name != "textarea";
inert_element_to_tokens(node, escape, global_class)
let el_name = el_node.name().to_string();
if is_svg_element(&el_name) && el_name != "svg" {
Some(inert_svg_element_to_tokens(
node,
escape,
global_class,
))
} else {
Some(inert_element_to_tokens(node, escape, global_class))
}
} else {
element_to_tokens(
el_node,

View File

@@ -119,3 +119,45 @@ fn returns_static_lifetime() {
WithLifetime(WithLifetimeProps::builder().data(&val).build())
}
}
// an attempt to catch unhygienic macros regression
mod macro_hygiene {
// To ensure no relative module path to leptos inside macros.
mod leptos {}
// doing this separately to below due to this being the smallest
// unit with the lowest import surface.
#[test]
fn view() {
use ::leptos::IntoView;
use ::leptos_macro::{component, view};
#[component]
fn Component() -> impl IntoView {
view! {
{()}
{()}
}
}
}
// may extend this test with other items as necessary.
#[test]
fn view_into_any() {
use ::leptos::{
prelude::{ElementChild, IntoAny},
IntoView,
};
use ::leptos_macro::{component, view};
#[component]
fn Component() -> impl IntoView {
view! {
<div>
{().into_any()}
{()}
</div>
}
}
}
}

View File

@@ -11,7 +11,7 @@ edition.workspace = true
[dependencies]
base64 = { workspace = true, default-features = true }
codee = { version = "0.3.0", features = ["json_serde"] }
codee = { features = ["json_serde"] , workspace = true, default-features = true }
hydration_context = { workspace = true }
reactive_graph = { workspace = true, features = ["hydration"] }
server_fn = { workspace = true }

View File

@@ -20,7 +20,7 @@ use reactive_graph::{
};
use std::{
future::{pending, Future, IntoFuture},
ops::DerefMut,
ops::{Deref, DerefMut},
panic::Location,
};
@@ -43,6 +43,14 @@ impl<T> Clone for ArcLocalResource<T> {
}
}
impl<T> Deref for ArcLocalResource<T> {
type Target = ArcAsyncDerived<T>;
fn deref(&self) -> &Self::Target {
&self.data
}
}
impl<T> ArcLocalResource<T> {
/// Creates the resource.
///
@@ -269,6 +277,14 @@ pub struct LocalResource<T> {
defined_at: &'static Location<'static>,
}
impl<T> Deref for LocalResource<T> {
type Target = AsyncDerived<T>;
fn deref(&self) -> &Self::Target {
&self.data
}
}
impl<T> Clone for LocalResource<T> {
fn clone(&self) -> Self {
*self

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_meta"
version = "0.8.2"
version = "0.8.3"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
@@ -10,12 +10,11 @@ edition.workspace = true
[dependencies]
leptos = { workspace = true }
once_cell = { workspace = true, default-features = true }
or_poisoned = { workspace = true }
indexmap = { workspace = true, default-features = true }
send_wrapper = { workspace = true, default-features = true }
tracing = { optional = true , workspace = true, default-features = true }
wasm-bindgen = { workspace = true , default-features = true }
tracing = { optional = true, workspace = true, default-features = true }
wasm-bindgen = { workspace = true, default-features = true }
futures = { workspace = true, default-features = true }
[dependencies.web-sys]

View File

@@ -63,13 +63,12 @@ use leptos::{
},
IntoView,
};
use once_cell::sync::Lazy;
use send_wrapper::SendWrapper;
use std::{
fmt::Debug,
sync::{
mpsc::{channel, Receiver, Sender},
Arc,
Arc, LazyLock,
},
};
use wasm_bindgen::JsCast;
@@ -101,7 +100,7 @@ pub struct MetaContext {
/// Metadata associated with the `<title>` element.
pub(crate) title: TitleContext,
/// The hydration cursor for the location in the `<head>` for arbitrary tags will be rendered.
pub(crate) cursor: Arc<Lazy<SendWrapper<Cursor>>>,
pub(crate) cursor: Arc<LazyLock<SendWrapper<Cursor>>>,
}
impl MetaContext {
@@ -143,7 +142,7 @@ impl Default for MetaContext {
))
};
let cursor = Arc::new(Lazy::new(build_cursor));
let cursor = Arc::new(LazyLock::new(build_cursor));
Self {
title: Default::default(),
cursor,
@@ -414,6 +413,7 @@ where
type Owned = RegisteredMetaTag<E, At::CloneableOwned, Ch::Owned>;
const MIN_LENGTH: usize = 0;
const EXISTS: bool = false;
fn dry_resolve(&mut self) {
self.el.dry_resolve()

View File

@@ -322,6 +322,7 @@ impl RenderHtml for TitleView {
type Owned = Self;
const MIN_LENGTH: usize = 0;
const EXISTS: bool = false;
fn dry_resolve(&mut self) {}

View File

@@ -21,7 +21,6 @@ fake = "2.9"
tokio-tungstenite = "0.23.1"
futures-util = "0.3.30"
uuid = { version = "1.10", features = ["serde"] }
once_cell = "1.19"
futures = "0.3.30"
[[test]]
@@ -33,6 +32,5 @@ harness = false # Allow Cucumber to print output instead of libtest
ssr = []
[dependencies]
once_cell = "1.19.0"
regex = "1.10.6"
serde.workspace = true

View File

@@ -18,14 +18,14 @@ use chromiumoxide::{
use cucumber::World;
use futures::channel::mpsc::Sender;
use futures_util::stream::StreamExt;
use once_cell::sync::Lazy;
use std::sync::LazyLock;
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, sync::Arc, time::Duration};
use tokio::sync::RwLock;
use tokio_tungstenite::connect_async;
use uuid::Uuid;
static EMAIL_ID_MAP: Lazy<RwLock<HashMap<String, String>>> =
Lazy::new(|| RwLock::new(HashMap::new()));
static EMAIL_ID_MAP: LazyLock<RwLock<HashMap<String, String>>> =
LazyLock::new(|| RwLock::new(HashMap::new()));
#[derive(Clone, Debug, PartialEq)]
pub struct RequestPair {
@@ -93,7 +93,7 @@ impl RequestPair {
async fn main() -> Result<()> {
// create a thread and store a
// tokio-tungstenite client that connectsto http://127.0.0.1:1080/ws
// and then stores the recieved messages in a once_cell::Lazy<RwLock<Vec<MailCrabMsg>>>
// and then stores the recieved messages in a std::sync::LazyLock<RwLock<Vec<MailCrabMsg>>>
// or a custom struct that matches the body or has specific impls for verify codes, links etc.
let _ = tokio::spawn(async move {
let (mut socket, _) = connect_async(
@@ -152,7 +152,7 @@ async fn main() -> Result<()> {
tokio::task::spawn(async move {
while let Some(event) = log_events.next().await {
if let Some(EventEntryAdded { entry }) =
if let Some(EventEntryAdded { entry }) =
Arc::<EventEntryAdded>::into_inner(event) {
console_logs.write().await.push(format!(" {entry:#?} "));
} else {
@@ -171,7 +171,7 @@ async fn main() -> Result<()> {
} else {
tracing::error!("tried to into inner but none")
}
}
});
@@ -208,7 +208,7 @@ async fn main() -> Result<()> {
thing.cookies_before_request = cookies;
}
}
CookieEnum::AfterResp(req_id) => {
let cookies = page
@@ -293,8 +293,8 @@ async fn main() -> Result<()> {
} else {
tracing::error!(" uhh err here")
}
}
});
// We don't need to join on our join handles, they will run detached and clean up whenever.

View File

@@ -1 +1,2 @@
/target
gen/

View File

@@ -5,4 +5,3 @@ members = ["src-tauri", "src-orig"]
[profile.release]
codegen-units = 1
lto = true

View File

@@ -1,20 +1,27 @@
# Tauri from scratch
This is a guide on how to build a leptos tauri project from scratch without using a template.
<br><br>
First
```sh
cargo new leptos_tauri_from_scratch
```
Then, make our two separate project folders. We need one for our actual app, 'src-orig' and the other is required when using `cargo tauri`
Then, make our two separate project folders. We need one for our actual app, _src-orig/_ and the other is required when using `cargo tauri`
```sh
mkdir src-orig && mkdir src-tauri
```
Delete the original src folder.
```sh
rm -r src
```
Rewrite the `Cargo.toml` file in our crate root to the following.
```toml
[workspace]
resolver = "2"
@@ -24,10 +31,13 @@ members = ["src-tauri", "src-orig"]
codegen-units = 1
lto = true
```
We'll use resolver two because we're using a modern version of Rust. We'll list our workspace members. `codegen-units = 1` and `lto = true` are good things to have for our eventual release, they make the wasm file smaller.
<br><br>
We'll list our workspace members. `codegen-units = 1` and `lto = true` are good things to have for our eventual release, they make the wasm file smaller.
What we're going to do is use `cargo leptos` for building our SSR server and we'll call trunk from `cargo tauri` for building our CSR client that we bundle into our different apps.
Let's add a `Trunk.toml` file.
```toml
[build]
target = "./src-orig/index.html"
@@ -37,7 +47,7 @@ ignore = ["./src-tauri"]
```
The target of `index.html` is what trunk uses to build the wasm and js files that we'll need for the bundling process when we call `cargo tauri build`. We'll get the resulting files in a `src-orig/dist` folder.
<br>
Create the `index.html` file
```sh
@@ -45,47 +55,95 @@ touch src-orig/index.html
```
Let's fill it with
```html
<!DOCTYPE html>
<html>
<head>
<link data-trunk rel="rust" data-wasm-opt="z" data-bin="leptos_tauri_from_scratch_bin"/>
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body></body>
<head>
<link
data-trunk
rel="rust"
data-wasm-opt="z"
data-bin="leptos_tauri_from_scratch_bin"
/>
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body></body>
</html>
```
This line
```html
<link data-trunk rel="rust" data-wasm-opt="z" data-bin="leptos_tauri_from_scratch_bin"/>
<link
data-trunk
rel="rust"
data-wasm-opt="z"
data-bin="leptos_tauri_from_scratch_bin"
/>
```
Tells trunk we want to compile our wasm to be small with `opt="z"` and that our binary will be named `"leptos_tauri_from_scratch_bin"`. <br>
We need to specify that our binary will be a different name then our project name because we are also going to get a wasm file from our library and if we don't use different names then `cargo tauri` will get confused. <br>
Tells trunk we want to compile our wasm to be small with `opt="z"` and that our binary will be named `"leptos_tauri_from_scratch_bin"`.
We need to specify that our binary will be a different name than our project name because we are also going to get a wasm file from our library and if we don't use different names then `cargo tauri` will get confused.
More specifically two wasm artifacts will be generated, one for the lib and the other for the binary and it won't know which to use.
<br><br>
Create a favicon that we referenced.
```sh
mkdir public
curl https://raw.githubusercontent.com/leptos-rs/leptos/main/examples/counter/public/favicon.ico > public/favicon.ico
```
mkdir public && curl https://raw.githubusercontent.com/leptos-rs/leptos/main/examples/animated_show/public/favicon.ico > public/favicon.ico
```
<br><br>
Let's create a tauri configuration file.
```sh
touch src-tauri/taur.conf.json
```
And drop this in there
```json
{
"identifier": "leptos.chat.app",
"productName": "leptos_tauri_from_scratch",
"version": "0.1.0",
"build": {
"beforeDevCommand": "",
"beforeBuildCommand": "trunk build --no-default-features -v --features \"csr\"",
"devPath": "http://127.0.0.1:3000",
"distDir": "../src-orig/dist"
"devUrl": "http://127.0.0.1:3000",
"frontendDist": "../dist"
},
"package": {
"productName": "leptos_tauri_from_scratch",
"version": "0.1.0"
"bundle": {
"active": true,
"category": "DeveloperTool",
"copyright": "",
"externalBin": [],
"icon": ["icons/icon.png"],
"longDescription": "",
"macOS": {
"entitlements": null,
"exceptionDomain": "",
"frameworks": [],
"providerShortName": null,
"signingIdentity": null
},
"resources": [],
"shortDescription": "",
"targets": "all",
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": ""
}
},
"tauri": {
"app": {
"security": {
"csp": null
},
"windows": [
{
"fullscreen": false,
@@ -94,69 +152,53 @@ And drop this in there
"title": "LeptosChatApp",
"width": 1200
}
],
"bundle": {
"active": true,
"category": "DeveloperTool",
"copyright": "",
"deb": {
"depends": []
},
"externalBin": [],
"icon": [],
"identifier": "leptos.chat.app",
"longDescription": "",
"macOS": {
"entitlements": null,
"exceptionDomain": "",
"frameworks": [],
"providerShortName": null,
"signingIdentity": null
},
"resources": [],
"shortDescription": "",
"targets": "all",
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": ""
}
},
"security": {
"csp": null
}
]
}
}
```
You can basically ignore all of this except for
```json
"build": {
"beforeDevCommand": "",
"beforeBuildCommand": "trunk build --no-default-features -v --features \"csr\"",
"devPath": "http://127.0.0.1:3000",
"distDir": "../src-orig/dist"
"devUrl": "http://127.0.0.1:3000",
"frontendDist": "../dist"
},
```
Let's look at
Let's look at
```json
"beforeBuildCommand": "trunk build --no-default-features -v --features \"csr\"",
```
When we `cargo tauri build` this will run before hand. Trunk will run it's build process, using the index.html file in the src-orig that we specified in `Trunk.toml` <br>
We'll build a binary using only the CSR feature. This is important. <br>
We are going to build an SSR app, and serve it over the internet but we are also going to build a tauri client for desktop and mobile using CSR.<br>
It's going to make network requests to our server that is servering our app to browsers using SSR.<br>
This is the best of both worlds, we get the SEO of SSR and other advantages while being able to use CSR to build our app for other platforms.
When we `cargo tauri build` this will run before hand. Trunk will run it's build process, using the index.html file in the src-orig that we specified in `Trunk.toml`.
We'll build a binary using only the CSR feature. This is important.
We are going to build an SSR app, and serve it over the internet but we are also going to build a tauri client for desktop and mobile using CSR.
It's going to make network requests to our server that is servering our app to browsers using SSR.
This is the best of both worlds, we get the SEO of SSR and other advantages while being able to use CSR to build our app for other platforms.
```json
"devUrl": "http://127.0.0.1:3000",
"frontendDist": "../dist"
```
"devPath": "http://127.0.0.1:3000",
"distDir": "../src-orig/dist"
```
Check https://tauri.app/v1/api/config/#buildconfig for what these do, but our before build command `trunk build` will build into a folder `src-orig/dist` which we reference here.
<br><br>
Check <https://tauri.app/v1/api/config/#buildconfig> for what these do, but our before build command `trunk build` will build into a folder `src-orig/dist` which we reference here.
Let's add a `Cargo.toml`` to both of our packages.
```sh
touch src-tauri/Cargo.toml && touch src-orig/Cargo.toml
```
Let's change `src-tauri/Cargo.toml` to this, we're using the 2.0.0 alpha version of tauri to be able to build to mobile.
Let's change `src-tauri/Cargo.toml` to this.
```toml
[package]
name = "src_tauri"
@@ -164,86 +206,88 @@ version = "0.0.1"
edition = "2021"
[lib]
name="app_lib"
path="src/lib.rs"
name = "app_lib"
path = "src/lib.rs"
[build-dependencies]
tauri-build = { version = "2.0.0-alpha.13", features = [] }
tauri-build = { version = "2.2.0", features = [] }
[dependencies]
log = "0.4.0"
log = "0.4.22"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "2.0.0-alpha.20", features = ["devtools"] }
tauri-plugin-http = "2.0.0-alpha.9"
tauri = { version = "2.5.1", features = ["devtools"] }
tauri-plugin-http = "2.4.4"
[features]
#default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"]
```
To make use of `cargo tauri build` we need `tauri-build` and we also need a `build.rs`
```
To make use of `cargo tauri build` we need `tauri-build` and we also need a `build.rs`.
```sh
touch src-tauri/build.rs
```
And let's change that to
```
```rust
fn main() {
tauri_build::build();
}
```
In our `src-orig/Cargo.toml` let's add.
```
```toml
[package]
name = "leptos_tauri_from_scratch"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["staticlib", "cdylib", "rlib"]
[[bin]]
name="leptos_tauri_from_scratch_bin"
path="./src/main.rs"
name = "leptos_tauri_from_scratch_bin"
path = "./src/main.rs"
[dependencies]
axum = {version = "0.7.0", optional=true}
axum-macros = { version= "0.4.1", optional=true}
cfg-if = "1.0.0"
console_error_panic_hook = "0.1.7"
console_log = "1.0.0"
leptos = { git = "https://github.com/leptos-rs/leptos.git", branch = "leptos_v0.6" }
leptos-use = "0.9.0"
leptos_axum = { git = "https://github.com/leptos-rs/leptos.git", branch = "leptos_v0.6", optional = true }
leptos_meta = { git = "https://github.com/leptos-rs/leptos.git", branch = "leptos_v0.6" }
leptos_router = { git = "https://github.com/leptos-rs/leptos.git", branch = "leptos_v0.6" }
log = "0.4.20"
serde = "1.0.195"
serde_json = "1.0.111"
server_fn = { git = "https://github.com/leptos-rs/leptos.git", branch = "leptos_v0.6" }
tokio = { version = "1.35.1", optional=true }
tower = {version = "0.4.10", optional = true}
tower-http = { version = "0.5.1", optional = true, features= ["fs","cors"] }
wasm-bindgen = "0.2.89"
axum = { version = "0.8.4", optional = true }
axum-macros = { version = "0.5.0", optional = true }
console_error_panic_hook = { version = "0.1.7", optional = true }
leptos = { git = "https://github.com/leptos-rs/leptos.git", rev = "v0.8.2" }
leptos_axum = { git = "https://github.com/leptos-rs/leptos.git", rev = "v0.8.2", optional = true }
leptos_meta = { git = "https://github.com/leptos-rs/leptos.git", rev = "v0.8.2", optional = true }
server_fn = { git = "https://github.com/leptos-rs/leptos.git", rev = "v0.8.2", optional = true }
tokio = { version = "1.45.1", features = ["rt-multi-thread"], optional = true }
tower = { version = "0.5.2", optional = true }
tower-http = { version = "0.5.2", features = ["fs", "cors"], optional = true }
wasm-bindgen = { version = "=0.2.100", optional = true }
[features]
csr = [ "leptos/csr","leptos_meta/csr","leptos_router/csr", ]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
csr = ["leptos/csr", "dep:server_fn"]
hydrate = [
"leptos/hydrate",
"dep:leptos_meta",
"dep:console_error_panic_hook",
"dep:wasm-bindgen"
]
ssr = [
"dep:axum",
"dep:axum-macros",
"leptos/ssr",
"leptos-use/ssr",
"dep:leptos_axum",
"leptos_meta/ssr",
"leptos_router/ssr",
"dep:tower-http",
"dep:tower",
"dep:tokio",
]
"dep:axum",
"dep:axum-macros",
"leptos/ssr",
"dep:leptos_axum",
"dep:leptos_meta",
"leptos_meta/ssr",
"dep:tower-http",
"dep:tower",
"dep:tokio",
]
[package.metadata.leptos]
bin-exe-name="leptos_tauri_from_scratch_bin"
output-name="leptos_tauri_from_scratch"
bin-exe-name = "leptos_tauri_from_scratch_bin"
output-name = "leptos_tauri_from_scratch"
assets-dir = "../public"
site-pkg-dir = "pkg"
site-root = "target/site"
@@ -257,169 +301,240 @@ bin-default-features = false
lib-features = ["hydrate"]
lib-default-features = false
```
So this looks like a normal SSR leptos, except for our CSR, Hydrate, and SSR versions.
```toml
[features]
csr = [ "leptos/csr","leptos_meta/csr","leptos_router/csr", ]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
csr = ["leptos/csr", "dep:server_fn"]
hydrate = [
"leptos/hydrate",
"dep:leptos_meta",
"dep:console_error_panic_hook",
"dep:wasm-bindgen"
]
ssr = [
```
also our binary is specified and named
```toml
[[bin]]
name="leptos_tauri_from_scratch_bin"
path="./src/main.rs"
```
our lib is specified, but unnamed (it will default to the project name in cargo leptos and in cargo tauri). We need the different crate types for `cargo leptos serve` and `cargo tauri build`
```toml
[lib]
crate-type = ["staticlib", "cdylib", "rlib"]
```
We've added the override to our cargo leptos metadata.
```toml
[package.metadata.leptos]
bin-exe-name="leptos_tauri_from_scratch_bin"
```
Our tauri app is going to send server function calls to this address, this is aksi where we'll serve our hydratable SSR client from.
```
Our tauri app is going to send server function calls to this address, this is where we'll serve our hydratable SSR client from.
```toml
site-addr = "0.0.0.0:3000"
```
Now let's create the `main.rs` that we reference in the `src-orig/Cargo.toml`
```
```sh
mkdir src-orig/src && touch src-orig/src/main.rs
```
and drop this in there...
```rust
cfg_if::cfg_if! {
if #[cfg(feature="ssr")] {
use tower_http::cors::{CorsLayer};
use axum::{
Router,
routing::get,
extract::State,
http::Request,
body::Body,
response::IntoResponse
};
use leptos::{*,provide_context, LeptosOptions};
use leptos_axum::LeptosRoutes;
use leptos_tauri_from_scratch::fallback::file_and_error_handler;
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use axum::{
body::Body,
extract::{Request, State},
response::IntoResponse,
routing::get,
Router,
};
use leptos::logging::log;
use leptos::prelude::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use leptos_tauri_from_scratch::{
app::{shell, App},
fallback::file_and_error_handler,
};
use tower_http::cors::CorsLayer;
#[derive(Clone,Debug,axum_macros::FromRef)]
pub struct ServerState{
pub options:LeptosOptions,
pub routes: Vec<leptos_router::RouteListing>,
}
let conf = get_configuration(None).unwrap();
let addr = conf.leptos_options.site_addr;
let leptos_options = conf.leptos_options;
// Generate the list of routes in your Leptos App
let routes = generate_route_list(App);
pub async fn server_fn_handler(
State(state): State<ServerState>,
request: Request<Body>,
) -> impl IntoResponse {
leptos_axum::handle_server_fns_with_context(
move || {
provide_context(state.clone());
},
request,
)
#[derive(Clone, Debug, axum_macros::FromRef)]
pub struct ServerState {
pub options: LeptosOptions,
pub routes: Vec<leptos_axum::AxumRouteListing>,
}
let state = ServerState {
options: leptos_options,
routes: routes.clone(),
};
pub async fn server_fn_handler(
State(state): State<ServerState>,
request: Request<Body>,
) -> impl IntoResponse {
leptos_axum::handle_server_fns_with_context(
move || {
provide_context(state.clone());
},
request,
)
.await
.into_response()
}
let cors = CorsLayer::new()
.allow_methods([axum::http::Method::GET, axum::http::Method::POST])
.allow_origin(
// Allow requests from the Tauri app
"tauri://localhost"
.parse::<axum::http::HeaderValue>()
.unwrap(),
)
.allow_headers(vec![
axum::http::header::CONTENT_TYPE,
axum::http::header::ACCEPT,
]);
pub async fn leptos_routes_handler(
State(state): State<ServerState>,
req: Request<Body>,
) -> axum::response::Response {
let leptos_options = state.options.clone();
let handler = leptos_axum::render_route_with_context(
state.routes.clone(),
move || {
provide_context("...");
},
move || shell(leptos_options.clone()),
);
handler(axum::extract::State(state), req)
.await
.into_response()
}
pub async fn leptos_routes_handler(
State(state): State<ServerState>,
req: Request<Body>,
) -> axum::response::Response {
let handler = leptos_axum::render_route_with_context(
state.options.clone(),
state.routes.clone(),
move || {
provide_context("...");
},
leptos_tauri_from_scratch::App,
);
handler(req).await.into_response()
}
#[tokio::main]
async fn main() {
let conf = get_configuration(Some("./src-orig/Cargo.toml")).await.unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
let routes = leptos_axum::generate_route_list(leptos_tauri_from_scratch::App);
let state = ServerState{
options:leptos_options,
routes:routes.clone(),
};
let cors = CorsLayer::new()
.allow_methods([axum::http::Method::GET, axum::http::Method::POST])
.allow_origin("tauri://localhost".parse::<axum::http::HeaderValue>().unwrap())
.allow_headers(vec![axum::http::header::CONTENT_TYPE]);
let app = Router::new()
.route("/api/*fn_name",get(server_fn_handler).post(server_fn_handler))
.layer(cors)
.leptos_routes_with_handler(routes, get(leptos_routes_handler))
.fallback(file_and_error_handler)
.with_state(state);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
logging::log!("listening on http://{}", &addr);
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}
} else if #[cfg(feature="csr")]{
pub fn main() {
server_fn::client::set_server_url("http://127.0.0.1:3000");
leptos::mount_to_body(leptos_tauri_from_scratch::App);
}
} else {
pub fn main() {
}
}
let app = Router::new()
.route(
"/api/{*fn_name}",
get(server_fn_handler).post(server_fn_handler),
)
.layer(cors)
.leptos_routes_with_handler(routes, get(leptos_routes_handler))
.fallback(file_and_error_handler)
.with_state(state);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
log!("listening on http://{}", &addr);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}
#[cfg(feature = "csr")]
pub fn main() {
server_fn::client::set_server_url("http://127.0.0.1:3000");
leptos::mount::mount_to_body(leptos_tauri_from_scratch::app::App);
}
```
This is our three pronged binary.
When we run cargo leptos server, we're going to get a server that is what's in our `if #[cfg(feature="ssr")] {` branch. We're going to hydrate, that's final `else` branch that is just empty. That actually gets filled in or something with a call to hydrate.
<br>
And our csr feature
and the hydration at `src-orig/src/lib.rs`
```rust
else if #[cfg(feature="csr")]{
pub fn main() {
server_fn::client::set_server_url("http://127.0.0.1:3000");
leptos::mount_to_body(leptos_tauri_from_scratch::App);
}
}
```
Here we're setting the server functions to use the url base that we access in our browser. I.e local host, on the port we specified in the leptos metadata.<br>
Otherwise our tauri app will try to route server function network requests using it's own idea of what it's url is. Which is `tauri://localhost` on macOS, and something else on windows.
<br>
Since we are going to be getting API requests from different locations beside our server's domain let's set up CORS, if you don't do this your tauri apps won't be able to make server function calls because it will run into CORS erros.
```rust
let cors = CorsLayer::new()
.allow_methods([axum::http::Method::GET, axum::http::Method::POST])
.allow_origin("tauri://localhost".parse::<axum::http::HeaderValue>().unwrap())
.allow_headers(vec![axum::http::header::CONTENT_TYPE]);
pub mod app;
#[cfg(feature = "ssr")]
pub mod fallback;
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
console_error_panic_hook::set_once();
leptos::mount::hydrate_body(app::App);
}
```
If you are on windows the origin of your app will be different then `tauri://localhost` and you'll need to figure that out, as well as if you deploy it to places that aren't your localhost!
<br>
This is our three pronged binary.
When we run cargo leptos server, we're going to get a server that is what's under `#[cfg(feature="ssr")]`.
And our csr feature
```rust
#[cfg(feature = "csr")]
pub fn main() {
server_fn::client::set_server_url("http://127.0.0.1:3000");
leptos::mount::mount_to_body(leptos_tauri_from_scratch::app::App);
}
```
Here we're setting the server functions to use the url base that we access in our browser. I.e local host, on the port we specified in the leptos metadata.
Otherwise our tauri app will try to route server function network requests using it's own idea of what it's url is. Which is `tauri://localhost` on macOS, and something else on windows.
Since we are going to be getting API requests from different locations beside our server's domain let's set up CORS, if you don't do this your tauri apps won't be able to make server function calls because it will run into CORS erros.
```rust
let cors = CorsLayer::new()
.allow_methods([axum::http::Method::GET, axum::http::Method::POST])
.allow_origin(
"tauri://localhost"
.parse::<axum::http::HeaderValue>()
.unwrap(),
)
.allow_headers(vec![axum::http::header::CONTENT_TYPE]);
```
If you are on windows the origin of your app will be different than `tauri://localhost` and you'll need to figure that out, as well as if you deploy it to places that aren't your localhost!
Everything else is standard leptos, so let's fill in the fallback and the lib really quick.
```sh
touch src-orig/src/lib.rs && touch src-orig/src/fallback.rs
```
Let's dump this bog standard leptos code in the `src-orig/src/lib.rs``
Let's dump this bog standard leptos code in the `src-orig/src/app.rs`
```rust
use leptos::*;
use leptos::prelude::*;
#[cfg(feature = "ssr")]
pub mod fallback;
pub fn shell(options: LeptosOptions) -> impl IntoView {
use leptos_meta::MetaTags;
view! {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<AutoReload options=options.clone() />
<HydrationScripts options/>
<MetaTags/>
</head>
<body>
<App/>
</body>
</html>
}
}
#[server(endpoint = "hello_world")]
pub async fn hello_world_server() -> Result<String, ServerFnError> {
@@ -428,9 +543,9 @@ pub async fn hello_world_server() -> Result<String, ServerFnError> {
#[component]
pub fn App() -> impl IntoView {
let action = create_server_action::<HelloWorldServer>();
let vals = create_rw_signal(String::new());
create_effect(move |_| {
let action = ServerAction::<HelloWorldServer>::new();
let vals = RwSignal::new(String::new());
Effect::new(move |_| {
if let Some(resp) = action.value().get() {
match resp {
Ok(val) => vals.set(val),
@@ -438,31 +553,22 @@ pub fn App() -> impl IntoView {
}
}
});
view! {<button
on:click=move |_| {
action.dispatch(HelloWorldServer{});
}
view! {
<button
on:click=move |_| {
action.dispatch(HelloWorldServer{});
}
>"Hello world."</button>
{
move || vals.get()
}
}
}
cfg_if::cfg_if! {
if #[cfg(feature = "hydrate")] {
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen]
pub fn hydrate() {
#[cfg(debug_assertions)]
console_error_panic_hook::set_once();
leptos::mount_to_body(App);
}
<br/><br/>
<span>"Server says: "</span>
{move || vals.get()}
}
}
```
and add this to `src-org/src/fallback.rs`
```rust
use axum::{
body::Body,
@@ -470,7 +576,7 @@ use axum::{
http::{Request, Response, StatusCode, Uri},
response::{IntoResponse, Response as AxumResponse},
};
use leptos::{view, LeptosOptions};
use leptos::{view, prelude::LeptosOptions};
use tower::ServiceExt;
use tower_http::services::ServeDir;
@@ -485,12 +591,17 @@ pub async fn file_and_error_handler(
if res.status() == StatusCode::OK {
res.into_response()
} else {
let handler = leptos_axum::render_app_to_stream(options.to_owned(), move || view! {404});
let handler = leptos_axum::render_app_to_stream(
move || view! {404},
);
handler(req).await.into_response()
}
}
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<Body>, (StatusCode, String)> {
async fn get_static_file(
uri: Uri,
root: &str,
) -> Result<Response<Body>, (StatusCode, String)> {
let req = Request::builder()
.uri(uri.clone())
.body(Body::empty())
@@ -504,11 +615,15 @@ async fn get_static_file(uri: Uri, root: &str) -> Result<Response<Body>, (Status
}
}
```
Let's fill in our src-tauri/src folder.
```
Let's fill in our `src-tauri/src/` folder.
```sh
mkdir src-tauri/src && touch src-tauri/src/main.rs && touch src-tauri/src/lib.rs
```
and drop this in `src-tauri/src/main.rs` This is standard tauri boilerplate.
```rust
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
@@ -517,7 +632,9 @@ fn main() {
app_lib::run();
}
```
and in `src-tauri/src/lib.rs`
```rust
use tauri::Manager;
@@ -527,7 +644,7 @@ pub fn run() {
.plugin(tauri_plugin_http::init())
.setup(|app| {
{
let window = app.get_window("main").unwrap();
let window = app.get_webview_window("main").unwrap();
window.open_devtools();
}
Ok(())
@@ -536,21 +653,30 @@ pub fn run() {
.expect("error while running tauri application");
}
```
We're gonna open devtools right away to see what is going on in our app. We need the tauri_http_plugin to make http calls, and generate_context reads our `tauri.conf.json` in the package in which its run.
<br><br>
We need an icon folder and an icon to build.
```sh
mkdir src-tauri/icons && curl https://raw.githubusercontent.com/tauri-apps/tauri/dev/examples/.icons/128x128.png > src-tauri/icons/icon.png
mkdir src-tauri/icons
curl https://raw.githubusercontent.com/tauri-apps/tauri/dev/examples/.icons/128x128.png > src-tauri/icons/icon.png
```
set nightly
```sh
rustup override set nightly
```
Then run
Then run
```sh
cargo leptos serve
```
You should get
You should get something like
```sh
➜ lepto_tauri_from_scratch git:(main) ✗ cargo leptos serve
Finished dev [unoptimized + debuginfo] target(s) in 0.60s
@@ -561,24 +687,33 @@ You should get
Serving at http://0.0.0.0:3000
listening on http://0.0.0.0:3000
```
Now open a new terminal and
```sh
cargo tauri build
```
> Install `tauri-cli` if you haven't already.
It'll build with csr before
```sh
Running beforeBuildCommand `trunk build --no-default-features -v --features "csr"`
```
and then you should have your app, I'm on macOS so here's what I get. It's for desktop.
```
```sh
Compiling src_tauri v0.0.1 (/Users/sam/Projects/lepto_tauri_from_scratch/src-tauri)
Finished release [optimized] target(s) in 2m 26s
Bundling leptos_tauri_from_scratch.app (/Users/sam/Projects/lepto_tauri_from_scratch/target/release/bundle/macos/leptos_tauri_from_scratch.app)
Bundling leptos_tauri_from_scratch_0.1.0_x64.dmg (/Users/sam/Projects/lepto_tauri_from_scratch/target/release/bundle/dmg/leptos_tauri_from_scratch_0.1.0_x64.dmg)
Running bundle_dmg.sh
```
Open run it and voilá. Click hello world button and read "Hey" from the server.
## Thoughts, Feedback, Criticism, Comments?
Send me any of the above, I'm @sjud on leptos discord. I'm always looking to improve and make these projects more helpful for the community. So please let me know how I can do that. Thanks!
Send me any of the above, I'm @sjud on leptos discord. I'm always looking to improve and make these projects more helpful for the community. So please let me know how I can do that. Thanks!

View File

@@ -3,7 +3,6 @@ name = "leptos_tauri_from_scratch"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["staticlib", "cdylib", "rlib"]
@@ -12,43 +11,35 @@ name = "leptos_tauri_from_scratch_bin"
path = "./src/main.rs"
[dependencies]
axum = { version = "0.7.5", optional = true }
axum-macros = { version = "0.4.1", optional = true }
cfg-if = "1.0"
console_error_panic_hook = "0.1.7"
console_log = "1.0"
leptos = { git = "https://github.com/leptos-rs/leptos.git", branch = "leptos_v0.6" }
leptos-use = "0.11.3"
leptos_axum = { git = "https://github.com/leptos-rs/leptos.git", branch = "leptos_v0.6", optional = true }
leptos_meta = { git = "https://github.com/leptos-rs/leptos.git", branch = "leptos_v0.6" }
leptos_router = { git = "https://github.com/leptos-rs/leptos.git", branch = "leptos_v0.6" }
log = "0.4.22"
sqlx = { version = "0.8.0", optional = true, features = [
"sqlite",
"runtime-tokio",
] }
serde = "1.0"
serde_json = "1.0"
server_fn = { git = "https://github.com/leptos-rs/leptos.git", branch = "leptos_v0.6" }
tokio = { version = "1.39", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", optional = true, features = ["fs", "cors"] }
wasm-bindgen = "0.2.92"
axum = { version = "0.8.4", optional = true }
axum-macros = { version = "0.5.0", optional = true }
console_error_panic_hook = { version = "0.1.7", optional = true }
leptos = { git = "https://github.com/leptos-rs/leptos.git", rev = "v0.8.2" }
leptos_axum = { git = "https://github.com/leptos-rs/leptos.git", rev = "v0.8.2", optional = true }
leptos_meta = { git = "https://github.com/leptos-rs/leptos.git", rev = "v0.8.2", optional = true }
server_fn = { git = "https://github.com/leptos-rs/leptos.git", rev = "v0.8.2", optional = true }
tokio = { version = "1.45.1", features = ["rt-multi-thread"], optional = true }
tower = { version = "0.5.2", optional = true }
tower-http = { version = "0.5.2", features = ["fs", "cors"], optional = true }
wasm-bindgen = { version = "=0.2.100", optional = true }
[features]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
csr = ["leptos/csr", "dep:server_fn"]
hydrate = [
"leptos/hydrate",
"dep:leptos_meta",
"dep:console_error_panic_hook",
"dep:wasm-bindgen"
]
ssr = [
"dep:axum",
"dep:axum-macros",
"leptos/ssr",
"leptos-use/ssr",
"dep:leptos_axum",
"dep:leptos_meta",
"leptos_meta/ssr",
"leptos_router/ssr",
"dep:tower-http",
"dep:tower",
"dep:sqlx",
"dep:tokio",
]
@@ -67,4 +58,3 @@ bin-features = ["ssr"]
bin-default-features = false
lib-features = ["hydrate"]
lib-default-features = false

View File

@@ -0,0 +1,51 @@
use leptos::prelude::*;
#[cfg(feature = "ssr")]
pub fn shell(options: LeptosOptions) -> impl IntoView {
use leptos_meta::MetaTags;
view! {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<AutoReload options=options.clone() />
<HydrationScripts options/>
<MetaTags/>
</head>
<body>
<App/>
</body>
</html>
}
}
#[server(endpoint = "hello_world")]
pub async fn hello_world_server() -> Result<String, ServerFnError> {
Ok("Hey.".to_string())
}
#[component]
pub fn App() -> impl IntoView {
let action = ServerAction::<HelloWorldServer>::new();
let vals = RwSignal::new(String::new());
Effect::new(move |_| {
if let Some(resp) = action.value().get() {
match resp {
Ok(val) => vals.set(val),
Err(err) => vals.set(format!("{err:?}")),
}
}
});
view! {
<button
on:click=move |_| {
action.dispatch(HelloWorldServer{});
}
>"Hello world."</button>
<br/><br/>
<span>"Server says: "</span>
{move || vals.get()}
}
}

View File

@@ -4,7 +4,7 @@ use axum::{
http::{Request, Response, StatusCode, Uri},
response::{IntoResponse, Response as AxumResponse},
};
use leptos::{view, LeptosOptions};
use leptos::{prelude::LeptosOptions, view};
use tower::ServiceExt;
use tower_http::services::ServeDir;
@@ -19,12 +19,15 @@ pub async fn file_and_error_handler(
if res.status() == StatusCode::OK {
res.into_response()
} else {
let handler = leptos_axum::render_app_to_stream(options.to_owned(), move || view! {404});
let handler = leptos_axum::render_app_to_stream(move || view! {404});
handler(req).await.into_response()
}
}
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<Body>, (StatusCode, String)> {
async fn get_static_file(
uri: Uri,
root: &str,
) -> Result<Response<Body>, (StatusCode, String)> {
let req = Request::builder()
.uri(uri.clone())
.body(Body::empty())
@@ -36,4 +39,4 @@ async fn get_static_file(uri: Uri, root: &str) -> Result<Response<Body>, (Status
format!("Something went wrong: {err}"),
)),
}
}
}

View File

@@ -1,44 +1,10 @@
use leptos::*;
pub mod app;
#[cfg(feature = "ssr")]
pub mod fallback;
#[server(endpoint = "hello_world")]
pub async fn hello_world_server() -> Result<String, ServerFnError> {
Ok("Hey.".to_string())
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
console_error_panic_hook::set_once();
leptos::mount::hydrate_body(app::App);
}
#[component]
pub fn App() -> impl IntoView {
let action = create_server_action::<HelloWorldServer>();
let vals = create_rw_signal(String::new());
create_effect(move |_| {
if let Some(resp) = action.value().get() {
match resp {
Ok(val) => vals.set(val),
Err(err) => vals.set(format!("{err:?}")),
}
}
});
view! {<button
on:click=move |_| {
action.dispatch(HelloWorldServer{});
}
>"Hello world."</button>
{
move || vals.get()
}
}
}
cfg_if::cfg_if! {
if #[cfg(feature = "hydrate")] {
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen]
pub fn hydrate() {
#[cfg(debug_assertions)]
console_error_panic_hook::set_once();
leptos::mount_to_body(App);
}
}
}

View File

@@ -1,93 +1,100 @@
cfg_if::cfg_if! {
if #[cfg(feature="ssr")] {
use tower_http::cors::{CorsLayer};
use axum::{
Router,
routing::get,
extract::State,
http::Request,
body::Body,
response::IntoResponse
};
use leptos::{*,provide_context, LeptosOptions};
use leptos_axum::LeptosRoutes;
use leptos_tauri_from_scratch::fallback::file_and_error_handler;
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use axum::{
body::Body,
extract::{Request, State},
response::IntoResponse,
routing::get,
Router,
};
use leptos::logging::log;
use leptos::prelude::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use leptos_tauri_from_scratch::{
app::{shell, App},
fallback::file_and_error_handler,
};
use tower_http::cors::CorsLayer;
#[derive(Clone,Debug,axum_macros::FromRef)]
pub struct ServerState{
pub options:LeptosOptions,
pub routes: Vec<leptos_router::RouteListing>,
}
let conf = get_configuration(None).unwrap();
let addr = conf.leptos_options.site_addr;
let leptos_options = conf.leptos_options;
// Generate the list of routes in your Leptos App
let routes = generate_route_list(App);
pub async fn server_fn_handler(
State(state): State<ServerState>,
request: Request<Body>,
) -> impl IntoResponse {
leptos_axum::handle_server_fns_with_context(
move || {
provide_context(state.clone());
},
request,
)
#[derive(Clone, Debug, axum_macros::FromRef)]
pub struct ServerState {
pub options: LeptosOptions,
pub routes: Vec<leptos_axum::AxumRouteListing>,
}
let state = ServerState {
options: leptos_options,
routes: routes.clone(),
};
pub async fn server_fn_handler(
State(state): State<ServerState>,
request: Request<Body>,
) -> impl IntoResponse {
leptos_axum::handle_server_fns_with_context(
move || {
provide_context(state.clone());
},
request,
)
.await
.into_response()
}
let cors = CorsLayer::new()
.allow_methods([axum::http::Method::GET, axum::http::Method::POST])
.allow_origin(
"tauri://localhost"
.parse::<axum::http::HeaderValue>()
.unwrap(),
)
.allow_headers(vec![axum::http::header::CONTENT_TYPE]);
pub async fn leptos_routes_handler(
State(state): State<ServerState>,
req: Request<Body>,
) -> axum::response::Response {
let leptos_options = state.options.clone();
let handler = leptos_axum::render_route_with_context(
state.routes.clone(),
move || {
provide_context("...");
},
move || shell(leptos_options.clone()),
);
handler(axum::extract::State(state), req)
.await
.into_response()
}
pub async fn leptos_routes_handler(
State(state): State<ServerState>,
req: Request<Body>,
) -> axum::response::Response {
let handler = leptos_axum::render_route_with_context(
state.options.clone(),
state.routes.clone(),
move || {
provide_context("...");
},
leptos_tauri_from_scratch::App,
);
handler(req).await.into_response()
}
#[tokio::main]
async fn main() {
let conf = get_configuration(Some("./src-orig/Cargo.toml")).await.unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
let routes = leptos_axum::generate_route_list(leptos_tauri_from_scratch::App);
let state = ServerState{
options:leptos_options,
routes:routes.clone(),
};
let cors = CorsLayer::new()
.allow_methods([axum::http::Method::GET, axum::http::Method::POST])
.allow_origin("tauri://localhost".parse::<axum::http::HeaderValue>().unwrap())
.allow_headers(vec![axum::http::header::CONTENT_TYPE]);
let app = Router::new()
.route("/api/*fn_name",get(server_fn_handler).post(server_fn_handler))
.layer(cors)
.leptos_routes_with_handler(routes, get(leptos_routes_handler))
.fallback(file_and_error_handler)
.with_state(state);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
logging::log!("listening on http://{}", &addr);
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}
} else if #[cfg(feature="csr")]{
pub fn main() {
server_fn::client::set_server_url("http://127.0.0.1:3000");
leptos::mount_to_body(leptos_tauri_from_scratch::App);
}
} else {
pub fn main() {
}
}
}
let app = Router::new()
.route(
"/api/{*fn_name}",
get(server_fn_handler).post(server_fn_handler),
)
.layer(cors)
.leptos_routes_with_handler(routes, get(leptos_routes_handler))
.fallback(file_and_error_handler)
.with_state(state);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
log!("listening on http://{}", &addr);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}
#[cfg(feature = "csr")]
pub fn main() {
server_fn::client::set_server_url("http://127.0.0.1:3000");
leptos::mount::mount_to_body(leptos_tauri_from_scratch::app::App);
}

View File

@@ -8,13 +8,13 @@ name = "app_lib"
path = "src/lib.rs"
[build-dependencies]
tauri-build = { version = "2.0.0-rc.2", features = [] }
tauri-build = { version = "2.2.0", features = [] }
[dependencies]
log = "0.4.22"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "2.0.0-rc.2", features = ["devtools"] }
tauri-plugin-http = "2.0.0-rc.0"
tauri = { version = "2.5.1", features = ["devtools"] }
tauri-plugin-http = "2.4.4"
[features]
#default = ["custom-protocol"]

View File

@@ -6,11 +6,11 @@ pub fn run() {
.plugin(tauri_plugin_http::init())
.setup(|app| {
{
let window = app.get_window("main").unwrap();
let window = app.get_webview_window("main").unwrap();
window.open_devtools();
}
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
}

View File

@@ -3,4 +3,4 @@
fn main() {
app_lib::run();
}
}

View File

@@ -1,42 +1,37 @@
{
"identifier": "leptos.chat.app",
"productName": "leptos_tauri_from_scratch",
"version": "0.1.0",
"build": {
"beforeDevCommand": "",
"beforeBuildCommand": "trunk build --no-default-features -v --features \"csr\"",
"devPath": "http://127.0.0.1:3000",
"distDir": "../src-orig/dist"
"devUrl": "http://127.0.0.1:3000",
"frontendDist": "../dist"
},
"package": {
"productName": "leptos_tauri_from_scratch",
"version": "0.1.0"
},
"tauri": {
"bundle": {
"active": true,
"category": "DeveloperTool",
"copyright": "",
"deb": {
"depends": []
},
"externalBin": [],
"icon": [],
"identifier": "leptos.chat.app",
"longDescription": "",
"macOS": {
"entitlements": null,
"exceptionDomain": "",
"frameworks": [],
"providerShortName": null,
"signingIdentity": null
},
"resources": [],
"shortDescription": "",
"targets": "all",
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": ""
}
"bundle": {
"active": true,
"category": "DeveloperTool",
"copyright": "",
"externalBin": [],
"icon": ["icons/icon.png"],
"longDescription": "",
"macOS": {
"entitlements": null,
"exceptionDomain": "",
"frameworks": [],
"providerShortName": null,
"signingIdentity": null
},
"resources": [],
"shortDescription": "",
"targets": "all",
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": ""
}
},
"app": {
"security": {
"csp": null
},

View File

@@ -1,6 +1,6 @@
[package]
name = "reactive_graph"
version = "0.2.2"
version = "0.2.3"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
@@ -16,19 +16,26 @@ futures = { workspace = true, default-features = true }
hydration_context = { workspace = true, optional = true }
pin-project-lite = { workspace = true, default-features = true }
rustc-hash = { workspace = true, default-features = true }
serde = { features = ["derive"], optional = true , workspace = true, default-features = true }
serde = { features = [
"derive",
], optional = true, workspace = true, default-features = true }
slotmap = { workspace = true, default-features = true }
thiserror = { workspace = true , default-features = true }
tracing = { optional = true , workspace = true, default-features = true }
thiserror = { workspace = true, default-features = true }
tracing = { optional = true, workspace = true, default-features = true }
guardian = { workspace = true, default-features = true }
async-lock = "3.4.0"
send_wrapper = { features = ["futures"] , workspace = true, default-features = true }
async-lock = { workspace = true, default-features = true }
send_wrapper = { features = [
"futures",
], workspace = true, default-features = true }
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
web-sys = { version = "0.3.77", features = ["console"] }
[dev-dependencies]
tokio = { features = ["rt-multi-thread", "macros"] , workspace = true, default-features = true }
tokio = { features = [
"rt-multi-thread",
"macros",
], workspace = true, default-features = true }
tokio-test = { workspace = true, default-features = true }
any_spawner = { workspace = true, features = ["futures-executor", "tokio"] }

View File

@@ -484,7 +484,10 @@ impl<T: 'static> ArcAsyncDerived<T> {
{
let fun = move || {
let fut = fun();
async move { SendOption::new(Some(fut.await)) }
let fut = async move { SendOption::new(Some(fut.await)) };
#[cfg(feature = "sandboxed-arenas")]
let fut = Sandboxed::new(fut);
fut
};
let initial_value = SendOption::new(initial_value);
let (this, _) = spawn_derived!(
@@ -518,9 +521,12 @@ impl<T: 'static> ArcAsyncDerived<T> {
{
let fun = move || {
let fut = fun();
ScopedFuture::new_untracked(async move {
let fut = ScopedFuture::new_untracked(async move {
SendOption::new(Some(fut.await))
})
});
#[cfg(feature = "sandboxed-arenas")]
let fut = Sandboxed::new(fut);
fut
};
let initial_value = SendOption::new(initial_value);
let (this, _) = spawn_derived!(
@@ -562,7 +568,10 @@ impl<T: 'static> ArcAsyncDerived<T> {
{
let fun = move || {
let fut = fun();
async move { SendOption::new_local(Some(fut.await)) }
let fut = async move { SendOption::new_local(Some(fut.await)) };
#[cfg(feature = "sandboxed-arenas")]
let fut = Sandboxed::new(fut);
fut
};
let initial_value = SendOption::new_local(initial_value);
let (this, _) = spawn_derived!(
@@ -598,7 +607,10 @@ impl<T: 'static> ArcAsyncDerived<T> {
let initial = SendOption::new_local(None::<T>);
let fun = move || {
let fut = fun();
async move { SendOption::new_local(Some(fut.await)) }
let fut = async move { SendOption::new_local(Some(fut.await)) };
#[cfg(feature = "sandboxed-arenas")]
let fut = Sandboxed::new(fut);
fut
};
let (this, _) = spawn_derived!(
crate::spawn_local,

View File

@@ -167,7 +167,6 @@ impl Owner {
.map(|parent| parent.read().or_poisoned().arena.clone())
.unwrap_or_default(),
paused: false,
joined_owners: Vec::new(),
})),
#[cfg(feature = "hydration")]
shared_context,
@@ -202,7 +201,6 @@ impl Owner {
#[cfg(feature = "sandboxed-arenas")]
arena: Default::default(),
paused: false,
joined_owners: Vec::new(),
})),
#[cfg(feature = "hydration")]
shared_context,
@@ -228,7 +226,6 @@ impl Owner {
#[cfg(feature = "sandboxed-arenas")]
arena,
paused,
joined_owners: Vec::new(),
})),
#[cfg(feature = "hydration")]
shared_context: self.shared_context.clone(),
@@ -291,12 +288,18 @@ impl Owner {
/// fill the same need as an "on unmount" function in other UI approaches, etc.
pub fn on_cleanup(fun: impl FnOnce() + Send + Sync + 'static) {
if let Some(owner) = Owner::current() {
owner
.inner
.write()
.or_poisoned()
.cleanups
.push(Box::new(fun));
let mut inner = owner.inner.write().or_poisoned();
#[cfg(feature = "sandboxed-arenas")]
let fun = {
let arena = Arc::clone(&inner.arena);
move || {
Arena::set(&arena);
fun()
}
};
inner.cleanups.push(Box::new(fun));
}
}
@@ -458,7 +461,6 @@ pub(crate) struct OwnerInner {
#[cfg(feature = "sandboxed-arenas")]
arena: Arc<RwLock<ArenaMap>>,
paused: bool,
joined_owners: Vec<Owner>,
}
impl Debug for OwnerInner {

View File

@@ -3,19 +3,9 @@ use or_poisoned::OrPoisoned;
use std::{
any::{Any, TypeId},
collections::VecDeque,
sync::Arc,
};
impl Owner {
#[doc(hidden)]
pub fn join_contexts(&self, other: &Owner) {
self.inner
.write()
.or_poisoned()
.joined_owners
.push(other.clone());
}
fn provide_context<T: Send + Sync + 'static>(&self, value: T) {
self.inner
.write()
@@ -35,27 +25,18 @@ impl Owner {
if let Some(context) = contexts.remove(&ty) {
context.downcast::<T>().ok().map(|n| *n)
} else {
let parent = inner.parent.as_ref().and_then(|p| p.upgrade());
let joined = inner
.joined_owners
.iter()
.map(|owner| Arc::clone(&owner.inner));
for parent in parent.into_iter().chain(joined) {
let mut parent = Some(parent);
while let Some(ref this_parent) = parent.clone() {
let mut this_parent = this_parent.write().or_poisoned();
let contexts = &mut this_parent.contexts;
let value = contexts.remove(&ty);
let downcast =
value.and_then(|context| context.downcast::<T>().ok());
if let Some(value) = downcast {
return Some(*value);
} else {
parent = this_parent
.parent
.as_ref()
.and_then(|p| p.upgrade());
}
let mut parent = inner.parent.as_ref().and_then(|p| p.upgrade());
while let Some(ref this_parent) = parent.clone() {
let mut this_parent = this_parent.write().or_poisoned();
let contexts = &mut this_parent.contexts;
let value = contexts.remove(&ty);
let downcast =
value.and_then(|context| context.downcast::<T>().ok());
if let Some(value) = downcast {
return Some(*value);
} else {
parent =
this_parent.parent.as_ref().and_then(|p| p.upgrade());
}
}
None
@@ -72,29 +53,21 @@ impl Owner {
let reference = if let Some(context) = contexts.get(&ty) {
context.downcast_ref::<T>()
} else {
let parent = inner.parent.as_ref().and_then(|p| p.upgrade());
let joined = inner
.joined_owners
.iter()
.map(|owner| Arc::clone(&owner.inner));
for parent in parent.into_iter().chain(joined) {
let mut parent = Some(parent);
while let Some(ref this_parent) = parent.clone() {
let this_parent = this_parent.read().or_poisoned();
let contexts = &this_parent.contexts;
let value = contexts.get(&ty);
let downcast =
value.and_then(|context| context.downcast_ref::<T>());
if let Some(value) = downcast {
return Some(cb(value));
} else {
parent = this_parent
.parent
.as_ref()
.and_then(|p| p.upgrade());
}
let mut parent = inner.parent.as_ref().and_then(|p| p.upgrade());
while let Some(ref this_parent) = parent.clone() {
let this_parent = this_parent.read().or_poisoned();
let contexts = &this_parent.contexts;
let value = contexts.get(&ty);
let downcast =
value.and_then(|context| context.downcast_ref::<T>());
if let Some(value) = downcast {
return Some(cb(value));
} else {
parent =
this_parent.parent.as_ref().and_then(|p| p.upgrade());
}
}
None
};
reference.map(cb)
@@ -110,27 +83,18 @@ impl Owner {
let reference = if let Some(context) = contexts.get_mut(&ty) {
context.downcast_mut::<T>()
} else {
let parent = inner.parent.as_ref().and_then(|p| p.upgrade());
let joined = inner
.joined_owners
.iter()
.map(|owner| Arc::clone(&owner.inner));
for parent in parent.into_iter().chain(joined) {
let mut parent = Some(parent);
while let Some(ref this_parent) = parent.clone() {
let mut this_parent = this_parent.write().or_poisoned();
let contexts = &mut this_parent.contexts;
let value = contexts.get_mut(&ty);
let downcast =
value.and_then(|context| context.downcast_mut::<T>());
if let Some(value) = downcast {
return Some(cb(value));
} else {
parent = this_parent
.parent
.as_ref()
.and_then(|p| p.upgrade());
}
let mut parent = inner.parent.as_ref().and_then(|p| p.upgrade());
while let Some(ref this_parent) = parent.clone() {
let mut this_parent = this_parent.write().or_poisoned();
let contexts = &mut this_parent.contexts;
let value = contexts.get_mut(&ty);
let downcast =
value.and_then(|context| context.downcast_mut::<T>());
if let Some(value) = downcast {
return Some(cb(value));
} else {
parent =
this_parent.parent.as_ref().and_then(|p| p.upgrade());
}
}
None

View File

@@ -1,6 +1,6 @@
[package]
name = "reactive_stores"
version = "0.2.2"
version = "0.2.3"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
@@ -11,7 +11,7 @@ edition.workspace = true
[dependencies]
guardian = { workspace = true, default-features = true }
itertools = { workspace = true , default-features = true }
itertools = { workspace = true, default-features = true }
or_poisoned = { workspace = true }
paste = { workspace = true, default-features = true }
reactive_graph = { workspace = true }
@@ -21,7 +21,10 @@ dashmap = { workspace = true, default-features = true }
send_wrapper = { workspace = true, default-features = true }
[dev-dependencies]
tokio = { features = ["rt-multi-thread", "macros"] , workspace = true, default-features = true }
tokio = { features = [
"rt-multi-thread",
"macros",
], workspace = true, default-features = true }
tokio-test = { workspace = true, default-features = true }
any_spawner = { workspace = true, features = ["futures-executor", "tokio"] }
reactive_graph = { workspace = true, features = ["effects"] }

View File

@@ -60,20 +60,16 @@ pub trait StoreField: Sized {
fn triggers_for_path(&self, path: StorePath) -> Vec<ArcTrigger> {
let trigger = self.get_trigger(path.clone());
let mut full_path = path;
full_path.pop();
// build a list of triggers, starting with the full path to this node and ending with the root
// this will mean that the root is the final item, and this path is first
let mut triggers = Vec::with_capacity(full_path.len());
let mut triggers = Vec::with_capacity(full_path.len() + 2);
triggers.push(trigger.this.clone());
triggers.push(trigger.children.clone());
loop {
while !full_path.is_empty() {
full_path.pop();
let inner = self.get_trigger(full_path.clone());
triggers.push(inner.children.clone());
if full_path.is_empty() {
break;
}
full_path.pop();
}
// when the WriteGuard is dropped, each trigger will be notified, in order

View File

@@ -114,21 +114,19 @@ where
#[track_caller]
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)
loop {
while !full_path.is_empty() {
full_path.pop();
let inner = self.get_trigger(full_path.clone());
inner.this.track();
if full_path.is_empty() {
break;
}
full_path.pop();
}
let trigger = self.get_trigger(self.path().into_iter().collect());
trigger.this.track();
trigger.children.track();
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "reactive_stores_macro"
version = "0.2.2"
version = "0.2.3"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
@@ -13,8 +13,8 @@ edition.workspace = true
proc-macro = true
[dependencies]
convert_case = { workspace = true , default-features = true }
convert_case = { workspace = true, default-features = true }
proc-macro-error2 = { workspace = true, default-features = true }
proc-macro2 = { workspace = true, default-features = true }
quote = { workspace = true, default-features = true }
syn = { features = ["full"] , workspace = true, default-features = true }
syn = { features = ["full"], workspace = true, default-features = true }

View File

@@ -111,10 +111,8 @@ impl ToTokens for Model {
} = &self;
let any_store_field = Ident::new("AnyStoreField", Span::call_site());
let trait_name = Ident::new(&format!("{name}StoreFields"), name.span());
let generics_with_orig = {
let params = &generics.params;
quote! { <#any_store_field, #params> }
};
let params = &generics.params;
let generics_with_orig = quote! { <#any_store_field, #params> };
let where_with_orig = {
generics
.where_clause
@@ -140,13 +138,13 @@ impl ToTokens for Model {
// read access
tokens.extend(quote! {
#vis trait #trait_name <AnyStoreField>
#vis trait #trait_name <AnyStoreField, #params>
#where_with_orig
{
#(#trait_fields)*
}
impl #generics_with_orig #trait_name <AnyStoreField> for AnyStoreField
impl #generics_with_orig #trait_name <AnyStoreField, #params> for AnyStoreField
#where_with_orig
{
#(#read_fields)*

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router"
version = "0.8.2"
version = "0.8.3"
authors = ["Greg Johnston", "Ben Wishovich"]
license = "MIT"
readme = "../README.md"
@@ -20,12 +20,11 @@ tachys = { workspace = true, features = ["reactive_graph"] }
futures = { workspace = true, default-features = true }
url = { workspace = true, default-features = true }
js-sys = { workspace = true, default-features = true }
wasm-bindgen = { workspace = true , default-features = true }
tracing = { optional = true , workspace = true, default-features = true }
once_cell = { workspace = true, default-features = true }
wasm-bindgen = { workspace = true, default-features = true }
tracing = { optional = true, workspace = true, default-features = true }
send_wrapper = { workspace = true, default-features = true }
thiserror = { workspace = true , default-features = true }
percent-encoding = { version = "2.3", optional = true }
thiserror = { workspace = true, default-features = true }
percent-encoding = { optional = true, workspace = true, default-features = true }
gloo-net = { workspace = true, default-features = true }
[dependencies.web-sys]

View File

@@ -4,7 +4,7 @@ use crate::location::RequestUrl;
pub use crate::nested_router::Outlet;
use crate::{
flat_router::FlatRoutesView,
hooks::use_navigate,
hooks::{use_matched, use_navigate},
location::{
BrowserUrl, Location, LocationChange, LocationProvider, State, Url,
},
@@ -144,16 +144,12 @@ impl RouterContext {
resolve_path("", path, None)
};
let mut url = match resolved_to.map(|to| BrowserUrl::parse(&to)) {
Some(Ok(url)) => url,
Some(Err(e)) => {
let mut url = match BrowserUrl::parse(&resolved_to) {
Ok(url) => url,
Err(e) => {
leptos::logging::error!("Error parsing URL: {e:?}");
return;
}
None => {
leptos::logging::error!("Error resolving relative URL.");
return;
}
};
let query_mutations =
mem::take(&mut *self.query_mutations.write_value());
@@ -203,7 +199,7 @@ impl RouterContext {
&'a self,
path: &'a str,
from: Option<&'a str>,
) -> Option<Cow<'a, str>> {
) -> Cow<'a, str> {
let base = self.base.as_deref().unwrap_or_default();
resolve_path(base, path, from)
}
@@ -580,7 +576,11 @@ pub fn Redirect<P>(
// redirect on the server
if let Some(redirect_fn) = use_context::<ServerRedirectFunction>() {
(redirect_fn.f)(&path);
(redirect_fn.f)(&resolve_path(
"",
&path,
Some(&use_matched().get_untracked()),
));
}
// redirect on the client
else {

View File

@@ -87,7 +87,7 @@ where
fn inner(
has_router: bool,
method: Option<&'static str>,
action: ArcMemo<Option<String>>,
action: ArcMemo<String>,
enctype: Option<String>,
version: Option<RwSignal<usize>>,
error: Option<RwSignal<Option<Box<dyn Error + Send + Sync>>>>,
@@ -311,7 +311,7 @@ where
let action = if has_router {
use_resolved_path(move || action.to_href()())
} else {
ArcMemo::new(move |_| Some(action.to_href()()))
ArcMemo::new(move |_| action.to_href()())
};
inner(
has_router,

View File

@@ -240,7 +240,7 @@ pub(crate) struct Matched(pub ArcMemo<String>);
#[track_caller]
pub(crate) fn use_resolved_path(
path: impl Fn() -> String + Send + Sync + 'static,
) -> ArcMemo<Option<String>> {
) -> ArcMemo<String> {
let router = use_context::<RouterContext>()
.expect("called use_resolved_path outside a <Router>");
// TODO make this work with flat routes too?
@@ -248,14 +248,14 @@ pub(crate) fn use_resolved_path(
ArcMemo::new(move |_| {
let path = path();
if path.starts_with('/') {
Some(path)
path
} else {
router
.resolve_path(
&path,
matched.as_ref().map(|n| n.get()).as_deref(),
)
.map(|n| n.to_string())
.to_string()
}
})
}

View File

@@ -66,6 +66,25 @@ where
/// This is helpful for accessibility and for styling. For example, maybe you want to set the link a
/// different color if its a link to the page youre currently on.
///
/// ### Additional Attributes
///
/// You can add additional HTML attributes to the `<a>` element created by this component using the attribute
/// spreading syntax for components. For example, to add a class, you can use `attr:class="my-link"`.
/// Alternately, you can add any number of HTML attributes (include `class`) after a `{..}` marker.
///
/// ```rust
/// # use leptos::prelude::*; use leptos_router::components::A;
/// # fn spread_example() -> impl IntoView {
/// view! {
/// <A href="/about" attr:class="my-link" {..} id="foo">"Some link"</A>
/// <A href="/about" {..} class="my-link" id="bar">"Another link"</A>
/// <A href="/about" {..} class:my-link=true id="baz">"One more"</A>
/// }
/// # }
/// ```
///
/// For more information on this attribute spreading syntax, [see here](https://book.leptos.dev/view/03_components.html#spreading-attributes-onto-components).
///
/// ### DOM Properties
///
/// `<a>` elements can take several additional DOM properties with special meanings.
@@ -102,7 +121,7 @@ where
H: ToHref + Send + Sync + 'static,
{
fn inner(
href: ArcMemo<Option<String>>,
href: ArcMemo<String>,
target: Option<Oco<'static, str>>,
exact: bool,
children: Children,
@@ -114,23 +133,21 @@ where
let is_active = {
let href = href.clone();
move || {
href.read().as_deref().is_some_and(|to| {
let path = to.split(['?', '#']).next().unwrap_or_default();
current_url.with(|loc| {
let loc = loc.path();
if exact {
loc == path
} else {
is_active_for(path, loc, strict_trailing_slash)
}
})
let path = normalize_path(&href.read());
current_url.with(|loc| {
let loc = loc.path();
if exact {
loc == path
} else {
is_active_for(&path, loc, strict_trailing_slash)
}
})
}
};
view! {
<a
href=move || href.get().unwrap_or_default()
href=move || href.get()
target=target
aria-current=move || if is_active() { Some("page") } else { None }
data-noscroll=!scroll
@@ -172,9 +189,62 @@ fn is_active_for(
}
}
// Resolve `".."` segments in the path. Assume path is either empty or starts with a `'/'``.
fn normalize_path(path: &str) -> String {
// Return only on the only condition where leading slash
// is allowed to be missing.
if path.is_empty() {
return String::new();
}
let mut del = 0;
let mut it = path
.split(['?', '#'])
.next()
.unwrap_or_default()
.split(['/'])
.rev()
.peekable();
let init = if it.peek() == Some(&"..") {
String::from("/")
} else {
String::new()
};
let mut path = it
.filter(|v| {
if *v == ".." {
del += 1;
false
} else if *v == "." {
false
} else if del > 0 {
del -= 1;
false
} else {
true
}
})
// We cannot reverse before the fold again bc the filter
// would be forwards again.
.fold(init, |mut p, v| {
p.reserve(v.len() + 1);
p.insert(0, '/');
p.insert_str(0, v);
p
});
path.truncate(path.len().saturating_sub(1));
// Path starts with '/' giving it an extra empty segment after the split
// Which should not be removed.
if !path.starts_with('/') {
path.insert(0, '/');
}
path
}
#[cfg(test)]
mod tests {
use super::is_active_for;
use super::{is_active_for, normalize_path};
#[test]
fn is_active_for_matched() {
@@ -393,4 +463,37 @@ mod tests {
// assert!(!is_same_level("/some/", "/some/level/"))
// assert!(!is_same_level("/some/", "/some/level/deeper"))
}
#[test]
fn normalize_path_test() {
// Make sure it doesn't touch already normalized urls.
assert!(normalize_path("") == "".to_string());
assert!(normalize_path("/") == "/".to_string());
assert!(normalize_path("/some") == "/some".to_string());
assert!(normalize_path("/some/") == "/some/".to_string());
// Correctly removes ".." segments.
assert!(normalize_path("/some/../another") == "/another".to_string());
assert!(
normalize_path("/one/two/../three/../../four")
== "/four".to_string()
);
// Correctly sets trailing slash if last segement is "..".
assert!(normalize_path("/one/two/..") == "/one/".to_string());
assert!(normalize_path("/one/two/../") == "/one/".to_string());
// Level outside of the url.
assert!(normalize_path("/..") == "/".to_string());
assert!(normalize_path("/../") == "/".to_string());
// Going into negative levels and coming back into the positives.
assert!(
normalize_path("/one/../../two/three") == "/two/three".to_string()
);
assert!(
normalize_path("/one/../../two/three/")
== "/two/three/".to_string()
);
}
}

View File

@@ -4,9 +4,9 @@ pub fn resolve_path<'a>(
base: &'a str,
path: &'a str,
from: Option<&'a str>,
) -> Option<Cow<'a, str>> {
) -> Cow<'a, str> {
if has_scheme(path) {
Some(path.into())
path.into()
} else {
let base_path = normalize(base, false);
let from_path = from.map(|from| normalize(from, false));
@@ -25,7 +25,7 @@ pub fn resolve_path<'a>(
let result_empty = result.is_empty();
let prefix = if result_empty { "/".into() } else { result };
Some(prefix + normalize(path, result_empty))
prefix + normalize(path, result_empty)
}
}

View File

@@ -65,6 +65,9 @@ where
// TODO loading fallback
#[allow(clippy::type_complexity)]
view: Rc<RefCell<EitherOf3State<(), Fal, AnyView>>>,
// held to keep the Owner alive until the router is dropped
#[allow(unused)]
outer_owner: Owner,
}
impl<Loc, Defs, FalFn, Fal> Render for NestedRoutesView<Loc, Defs, FalFn>
@@ -106,10 +109,10 @@ where
base,
&mut loaders,
&mut outlets,
&outer_owner,
);
drop(url);
outer_owner.with(|| EitherOf3::C(Outlet().into_any()))
EitherOf3::C(top_level_outlet(&outlets, &outer_owner))
}
};
@@ -130,6 +133,7 @@ where
current_url,
outlets,
view,
outer_owner,
}
}
@@ -175,7 +179,6 @@ where
&mut preloaders,
&mut full_loaders,
&mut state.outlets,
&self.outer_owner,
self.set_is_routing.is_some(),
0,
);
@@ -212,17 +215,14 @@ where
// if it was on the fallback, show the view instead
if matches!(state.view.borrow().state, EitherOf3::B(_)) {
self.outer_owner.with(|| {
EitherOf3::<(), Fal, AnyView>::C(Outlet().into_any())
.rebuild(&mut *state.view.borrow_mut());
})
EitherOf3::<(), Fal, AnyView>::C(top_level_outlet(
&state.outlets,
&self.outer_owner,
))
.rebuild(&mut *state.view.borrow_mut());
}
}
}
if let Some(outlet) = state.outlets.first() {
self.outer_owner.with(|| outlet.provide_contexts());
}
}
}
@@ -338,7 +338,6 @@ where
base,
&mut loaders,
&mut outlets,
&outer_owner,
);
// outlets will not send their views if the loaders are never polled
@@ -348,7 +347,7 @@ where
.now_or_never()
.expect("async routes not supported in SSR");
outer_owner.with(|| Either::Right(Outlet().into_any()))
Either::Right(top_level_outlet(&outlets, &outer_owner))
}
};
view.to_html_with_buf(
@@ -392,7 +391,6 @@ where
base,
&mut loaders,
&mut outlets,
&outer_owner,
);
// outlets will not send their views if the loaders are never polled
@@ -402,7 +400,7 @@ where
.now_or_never()
.expect("async routes not supported in SSR");
outer_owner.with(|| Either::Right(Outlet().into_any()))
Either::Right(top_level_outlet(&outlets, &outer_owner))
}
};
view.to_html_async_with_buf::<OUT_OF_ORDER>(
@@ -446,7 +444,6 @@ where
base,
&mut loaders,
&mut outlets,
&outer_owner,
);
drop(url);
@@ -454,7 +451,7 @@ where
join_all(mem::take(&mut loaders))
.now_or_never()
.expect("async routes not supported in SSR");
outer_owner.with(|| EitherOf3::C(Outlet().into_any()))
EitherOf3::C(top_level_outlet(&outlets, &outer_owner))
}
}
.hydrate::<FROM_SERVER>(cursor, position),
@@ -465,6 +462,7 @@ where
current_url,
outlets,
view,
outer_owner,
}
}
@@ -480,12 +478,16 @@ pub(crate) struct RouteContext {
trigger: ArcTrigger,
url: ArcRwSignal<Url>,
params: ArcRwSignal<ParamsMap>,
owner: Owner,
pub matched: ArcRwSignal<String>,
base: Option<Oco<'static, str>>,
view_fn: Arc<Mutex<OutletViewFn>>,
owner: Arc<Mutex<Option<Owner>>>,
child: ChildRoute,
}
#[derive(Clone)]
pub(crate) struct ChildRoute(Arc<Mutex<Option<RouteContext>>>);
impl Debug for RouteContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RouteContext")
@@ -493,19 +495,12 @@ impl Debug for RouteContext {
.field("trigger", &self.trigger)
.field("url", &self.url)
.field("params", &self.params)
.field("owner", &self.owner.debug_id())
.field("matched", &self.matched)
.field("base", &self.base)
.finish_non_exhaustive()
}
}
impl RouteContext {
fn provide_contexts(&self) {
provide_context(self.clone());
}
}
impl Clone for RouteContext {
fn clone(&self) -> Self {
Self {
@@ -513,10 +508,11 @@ impl Clone for RouteContext {
id: self.id,
trigger: self.trigger.clone(),
params: self.params.clone(),
owner: self.owner.clone(),
matched: self.matched.clone(),
base: self.base.clone(),
view_fn: Arc::clone(&self.view_fn),
owner: Arc::clone(&self.owner),
child: self.child.clone(),
}
}
}
@@ -528,7 +524,6 @@ trait AddNestedRoute {
base: Option<Oco<'static, str>>,
loaders: &mut Vec<Pin<Box<dyn Future<Output = ArcTrigger>>>>,
outlets: &mut Vec<RouteContext>,
parent: &Owner,
);
#[allow(clippy::too_many_arguments)]
@@ -538,9 +533,8 @@ trait AddNestedRoute {
base: Option<Oco<'static, str>>,
items: &mut usize,
loaders: &mut Vec<Pin<Box<dyn Future<Output = ArcTrigger>>>>,
full_loaders: &mut Vec<oneshot::Receiver<()>>,
full_loaders: &mut Vec<oneshot::Receiver<Option<Owner>>>,
outlets: &mut Vec<RouteContext>,
parent: &Owner,
set_is_routing: bool,
level: u8,
) -> u8;
@@ -556,15 +550,9 @@ where
base: Option<Oco<'static, str>>,
loaders: &mut Vec<Pin<Box<dyn Future<Output = ArcTrigger>>>>,
outlets: &mut Vec<RouteContext>,
parent: &Owner,
) {
let orig_url = url;
// each Outlet gets its own owner, so it can inherit context from its parent route,
// a new owner will be constructed if a different route replaces this one in the outlet,
// so that any signals it creates or context it provides will be cleaned up
let owner = parent.child();
// the params signal can be updated to allow the same outlet to update to changes in the
// params, even if there's not a route match change
let params = ArcRwSignal::new(self.to_params().into_iter().collect());
@@ -622,71 +610,82 @@ where
url,
trigger: trigger.clone(),
params,
owner: owner.clone(),
matched,
view_fn: Arc::new(Mutex::new(Box::new(|_owner| {
Suspend::new(Box::pin(async { ().into_any() }))
}))),
base: base.clone(),
child: ChildRoute(Arc::new(Mutex::new(None))),
owner: Arc::new(Mutex::new(None)),
};
if !outlets.is_empty() {
let prev_index = outlets.len().saturating_sub(1);
*outlets[prev_index].child.0.lock().or_poisoned() =
Some(outlet.clone());
}
outlets.push(outlet.clone());
// send the initial view through the channel, and recurse through the children
let (view, child) = self.into_view_and_child();
loaders.push(Box::pin(owner.with(|| {
ScopedFuture::new({
let owner = outlet.owner.clone();
let url = outlet.url.clone();
let matched = Matched(matched_including_parents);
let view_fn = Arc::clone(&outlet.view_fn);
async move {
provide_context(params_including_parents);
provide_context(url);
provide_context(matched.clone());
view.preload().await;
*view_fn.lock().or_poisoned() =
Box::new(move |owner_where_used| {
owner.join_contexts(&owner_where_used);
let view = view.clone();
owner.with({
let matched = matched.clone();
move || {
Suspend::new(Box::pin(async move {
let view = SendWrapper::new(
ScopedFuture::new(view.choose()),
);
let view = view.await;
let view = MatchedRoute(
matched.0.get_untracked(),
view,
);
OwnedView::new(view).into_any()
})
as Pin<
Box<
dyn Future<Output = AnyView>
+ Send,
>,
>)
}
})
});
trigger
}
})
})));
loaders.push(Box::pin(ScopedFuture::new({
let url = outlet.url.clone();
let matched = Matched(matched_including_parents);
let view_fn = Arc::clone(&outlet.view_fn);
let route_owner = Arc::clone(&outlet.owner);
let outlet = outlet.clone();
let params = params_including_parents.clone();
let url = url.clone();
let matched = matched.clone();
async move {
view.preload().await;
let child = outlet.child.clone();
*view_fn.lock().or_poisoned() =
Box::new(move |owner_where_used| {
*route_owner.lock().or_poisoned() =
Some(owner_where_used.clone());
let view = view.clone();
let child = child.clone();
let params = params.clone();
let url = url.clone();
let matched = matched.clone();
owner_where_used.with({
let matched = matched.clone();
move || {
let child = child.clone();
Suspend::new(Box::pin(async move {
provide_context(child.clone());
provide_context(params.clone());
provide_context(url.clone());
provide_context(matched.clone());
let view = SendWrapper::new(
ScopedFuture::new(view.choose()),
);
let view = view.await;
let view = MatchedRoute(
matched.0.get_untracked(),
view,
);
// and share the outlet with the parent via context
// we share it with the *parent* because the <Outlet/> is rendered in or below the parent
// wherever it appears, <Outlet/> will look for the closest RouteContext
parent.with(|| outlet.provide_contexts());
OwnedView::new(view).into_any()
})
as Pin<
Box<
dyn Future<Output = AnyView> + Send,
>,
>)
}
})
});
trigger
}
})));
// recursively continue building the tree
// 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, &owner);
child.build_nested_route(orig_url, base, loaders, outlets);
}
}
@@ -697,9 +696,8 @@ where
base: Option<Oco<'static, str>>,
items: &mut usize,
preloaders: &mut Vec<Pin<Box<dyn Future<Output = ArcTrigger>>>>,
full_loaders: &mut Vec<oneshot::Receiver<()>>,
full_loaders: &mut Vec<oneshot::Receiver<Option<Owner>>>,
outlets: &mut Vec<RouteContext>,
parent: &Owner,
set_is_routing: bool,
level: u8,
) -> u8 {
@@ -708,11 +706,17 @@ where
.take(*items)
.map(|route| (route.params.clone(), route.matched.clone()))
.unzip();
if outlets.get(*items).is_some() && *items > 0 {
*outlets[*items - 1].child.0.lock().or_poisoned() =
Some(outlets[*items].clone());
}
let current = outlets.get_mut(*items);
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, parent);
self.build_nested_route(url, base, preloaders, outlets);
level
}
Some(current) => {
@@ -779,70 +783,74 @@ where
// assign a new owner, so that contexts and signals owned by the previous route
// in this outlet can be dropped
let mut old_owner =
Some(mem::replace(&mut current.owner, parent.child()));
let owner = current.owner.clone();
let (full_tx, full_rx) = oneshot::channel();
let full_tx = Mutex::new(Some(full_tx));
full_loaders.push(full_rx);
let outlet = current.clone();
// send the new view, with the new owner, through the channel to the Outlet,
// and notify the trigger so that the reactive view inside the Outlet tracking
// the trigger runs again
preloaders.push(Box::pin(owner.with(|| {
ScopedFuture::new({
let owner = owner.clone();
let trigger = current.trigger.clone();
let url = current.url.clone();
let matched = Matched(matched_including_parents);
let view_fn = Arc::clone(&current.view_fn);
async move {
provide_context(params_including_parents);
provide_context(url);
provide_context(matched);
view.preload().await;
*view_fn.lock().or_poisoned() =
Box::new(move |owner_where_used| {
owner.join_contexts(&owner_where_used);
let owner = owner.clone();
let view = view.clone();
let full_tx =
full_tx.lock().or_poisoned().take();
let old_owner = old_owner.take();
Suspend::new(Box::pin(async move {
let view = SendWrapper::new(
owner.with(|| {
ScopedFuture::new(
async move {
if set_is_routing {
AsyncTransition::run(|| view.choose()).await
} else {
view.choose().await
}
}
)
}),
);
let view = view.await;
if let Some(old_owner) = old_owner {
old_owner.cleanup();
}
preloaders.push(Box::pin(ScopedFuture::new({
let trigger = current.trigger.clone();
let url = current.url.clone();
let matched = Matched(matched_including_parents);
let view_fn = Arc::clone(&current.view_fn);
let route_owner = Arc::clone(&current.owner);
let child = outlet.child.clone();
async move {
view.preload().await;
let child = child.clone();
*view_fn.lock().or_poisoned() =
Box::new(move |owner_where_used| {
let prev_owner = route_owner
.lock()
.or_poisoned()
.replace(owner_where_used.clone());
let view = view.clone();
let full_tx =
full_tx.lock().or_poisoned().take();
let child = child.clone();
let params =
params_including_parents.clone();
let url = url.clone();
let matched = matched.clone();
Suspend::new(Box::pin(async move {
let view = SendWrapper::new(
owner_where_used.with(|| {
provide_context(child.clone());
provide_context(params);
provide_context(url);
provide_context(matched);
ScopedFuture::new(async move {
if set_is_routing {
AsyncTransition::run(
|| view.choose(),
)
.await
} else {
view.choose().await
}
})
}),
);
if let Some(tx) = full_tx {
_ = tx.send(());
}
owner.with(|| {
OwnedView::new(view).into_any()
})
}))
});
let view = view.await;
drop(old_params);
drop(old_url);
drop(old_matched);
trigger
}
})
if let Some(tx) = full_tx {
_ = tx.send(prev_owner);
}
owner_where_used.with(|| {
OwnedView::new(view).into_any()
})
}))
});
drop(old_params);
drop(old_url);
drop(old_matched);
trigger
}
})));
// remove all the items lower in the tree
@@ -851,9 +859,10 @@ 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, &owner,
);
child
.build_nested_route(url, base, preloaders, outlets);
} else {
*outlets[*items].child.0.lock().or_poisoned() = None;
}
return level;
@@ -865,7 +874,6 @@ where
current.params.set(new_params);
current.url.set(url.to_owned());
if let Some(child) = child {
let owner = current.owner.clone();
*items += 1;
child.rebuild_nested_route(
url,
@@ -874,11 +882,11 @@ where
preloaders,
full_loaders,
outlets,
&owner,
set_is_routing,
level + 1,
)
} else {
*current.child.0.lock().or_poisoned() = None;
level
}
}
@@ -911,20 +919,38 @@ where
}
}
fn top_level_outlet(outlets: &[RouteContext], outer_owner: &Owner) -> AnyView {
let outlet = outlets.first().unwrap();
let child = outlet.child.clone();
let view_fn = outlet.view_fn.clone();
let trigger = outlet.trigger.clone();
outer_owner.clone().with(|| {
provide_context(child.clone());
let outer_owner = outer_owner.clone();
(move || {
trigger.track();
let mut view_fn = view_fn.lock().or_poisoned();
view_fn(outer_owner.child())
})
.into_any()
})
}
/// Displays the child route nested in a parent route, allowing you to control exactly where
/// that child route is displayed. Renders nothing if there is no nested child.
#[component]
pub fn Outlet() -> impl RenderHtml
where
{
move || {
let ctx = use_context::<RouteContext>()
.expect("<Outlet/> used without RouteContext being provided.");
let RouteContext {
trigger, view_fn, ..
} = ctx;
trigger.track();
let mut view_fn = view_fn.lock().or_poisoned();
view_fn(Owner::current().unwrap())
}
let ChildRoute(child) = use_context()
.expect("<Outlet/> used without RouteContext being provided.");
let child = child.lock().or_poisoned().clone();
let outer_owner = Owner::current().unwrap();
child.map(|child| {
move || {
child.trigger.track();
let mut view_fn = child.view_fn.lock().or_poisoned();
view_fn(outer_owner.child())
}
})
}

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router_macro"
version = "0.8.2"
version = "0.8.3"
authors = ["Greg Johnston", "Ben Wishovich"]
license = "MIT"
readme = "../README.md"
@@ -13,10 +13,10 @@ edition.workspace = true
proc-macro = true
[dependencies]
proc-macro-error2 = { default-features = false , workspace = true }
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 }
syn = { features = ["full"], workspace = true, default-features = true }
[dev-dependencies]
leptos_router = { path = "../router" }

View File

@@ -14,8 +14,8 @@ throw_error = { workspace = true }
server_fn_macro_default = { workspace = true }
# used for hashing paths in #[server] macro
const_format = { workspace = true, default-features = true }
const-str = "0.6.2"
rustversion = "1.0"
const-str = { workspace = true, default-features = true }
rustversion = { workspace = true, default-features = true }
xxhash-rust = { features = [
"const_xxh64",
], workspace = true, default-features = true }
@@ -27,39 +27,42 @@ send_wrapper = { features = [
thiserror = { workspace = true, default-features = true }
# registration system
inventory = { version = "0.3.20", optional = true }
inventory = { optional = true, workspace = true, default-features = true }
dashmap = { workspace = true, default-features = true }
once_cell = { workspace = true, default-features = true }
## servers
# actix
actix-web = { optional = true, workspace = true, default-features = true }
actix-ws = { version = "0.3.0", optional = true }
actix-ws = { optional = true, workspace = true, default-features = true }
# axum
axum = { optional = true, default-features = false, features = [
"multipart",
], workspace = true }
tower = { optional = true, workspace = true, default-features = true }
tower-layer = { version = "0.3.3", optional = true }
tower-layer = { optional = true, workspace = true, default-features = true }
## input encodings
serde_qs = { workspace = true, default-features = true }
multer = { version = "3.1", optional = true }
multer = { optional = true, workspace = true, default-features = true }
## output encodings
# serde
serde_json = { workspace = true, default-features = true }
serde-lite = { version = "0.5.0", features = ["derive"], optional = true }
serde-lite = { features = [
"derive",
], optional = true, workspace = true, default-features = true }
futures = { workspace = true, default-features = true }
http = { version = "1.3" }
ciborium = { version = "0.2.2", optional = true }
postcard = { version = "1", features = ["alloc"], optional = true }
hyper = { version = "1.6", optional = true }
bytes = "1.10"
http-body-util = { version = "0.1.3", optional = true }
rkyv = { version = "0.8.10", optional = true }
rmp-serde = { version = "1.3.0", optional = true }
http = { workspace = true, default-features = true }
ciborium = { optional = true, workspace = true, default-features = true }
postcard = { features = [
"alloc",
], optional = true, workspace = true, default-features = true }
hyper = { optional = true, workspace = true, default-features = true }
bytes = { workspace = true, default-features = true }
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 }
# client
@@ -67,7 +70,7 @@ gloo-net = { optional = true, workspace = true, default-features = true }
js-sys = { optional = true, workspace = true, default-features = true }
wasm-bindgen = { workspace = true, optional = true, default-features = true }
wasm-bindgen-futures = { optional = true, workspace = true, default-features = true }
wasm-streams = { version = "0.4.2", optional = true }
wasm-streams = { optional = true, workspace = true, default-features = true }
web-sys = { optional = true, features = [
"console",
"ReadableStream",
@@ -77,11 +80,11 @@ web-sys = { optional = true, features = [
], workspace = true, default-features = true }
# reqwest client
reqwest = { version = "0.12.15", default-features = false, optional = true, features = [
reqwest = { default-features = false, optional = true, features = [
"multipart",
"stream",
] }
tokio-tungstenite = { version = "0.26.2", optional = true }
], workspace = true }
tokio-tungstenite = { optional = true, workspace = true, default-features = true }
url = { workspace = true, default-features = true }
pin-project-lite = { workspace = true, default-features = true }
tokio = { features = [

View File

@@ -151,7 +151,6 @@ use error::{FromServerFnError, ServerFnErrorErr};
use futures::{pin_mut, SinkExt, Stream, StreamExt};
use http::Method;
use middleware::{BoxedService, Layer, Service};
use once_cell::sync::Lazy;
use redirect::call_redirect_hook;
use request::Req;
use response::{ClientRes, Res, TryRes};
@@ -169,7 +168,7 @@ use std::{
marker::PhantomData,
ops::{Deref, DerefMut},
pin::Pin,
sync::Arc,
sync::{Arc, LazyLock},
};
#[doc(hidden)]
pub use xxhash_rust;
@@ -862,7 +861,7 @@ pub use inventory;
#[macro_export]
macro_rules! initialize_server_fn_map {
($req:ty, $res:ty) => {
once_cell::sync::Lazy::new(|| {
std::sync::LazyLock::new(|| {
$crate::inventory::iter::<ServerFnTraitObj<$req, $res>>
.into_iter()
.map(|obj| {
@@ -981,7 +980,7 @@ impl<Req, Res> Clone for ServerFnTraitObj<Req, Res> {
#[allow(unused)] // used by server integrations
type LazyServerFnMap<Req, Res> =
Lazy<DashMap<(String, Method), ServerFnTraitObj<Req, Res>>>;
LazyLock<DashMap<(String, Method), ServerFnTraitObj<Req, Res>>>;
#[cfg(feature = "ssr")]
impl<Req: 'static, Res: 'static> inventory::Collect

View File

@@ -75,6 +75,13 @@ impl DerefMut for BrowserRequest {
#[derive(Debug)]
pub struct BrowserFormData(pub(crate) SendWrapper<FormData>);
impl BrowserFormData {
/// Returns the raw `web_sys::FormData` struct.
pub fn take(self) -> FormData {
self.0.take()
}
}
impl From<FormData> for BrowserFormData {
fn from(value: FormData) -> Self {
Self(SendWrapper::new(value))

View File

@@ -5,14 +5,14 @@ use crate::{
};
use bytes::Bytes;
use futures::{Stream, StreamExt};
use once_cell::sync::Lazy;
use reqwest::{
header::{ACCEPT, CONTENT_TYPE},
Body,
};
pub use reqwest::{multipart::Form, Client, Method, Request, Url};
use std::sync::LazyLock;
pub(crate) static CLIENT: Lazy<Client> = Lazy::new(Client::new);
pub(crate) static CLIENT: LazyLock<Client> = LazyLock::new(Client::new);
impl<E> ClientReq<E> for Request
where

View File

@@ -1,6 +1,6 @@
[package]
name = "tachys"
version = "0.2.3"
version = "0.2.4"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
@@ -20,10 +20,9 @@ reactive_graph = { workspace = true, optional = true }
reactive_stores = { workspace = true, optional = true }
slotmap = { optional = true, workspace = true, default-features = true }
oco_ref = { workspace = true, optional = true }
async-trait = "0.1.88"
once_cell = { workspace = true, default-features = true }
async-trait = { workspace = true, default-features = true }
paste = { workspace = true, default-features = true }
erased = "0.1.2"
erased = { workspace = true, default-features = true }
wasm-bindgen = { workspace = true, default-features = true }
html-escape = { workspace = true, default-features = true }
js-sys = { workspace = true, default-features = true }
@@ -149,18 +148,18 @@ web-sys = { features = [
"HtmlTemplateElement",
"HtmlOptionElement",
], workspace = true, default-features = true }
drain_filter_polyfill = "0.1.3"
drain_filter_polyfill = { workspace = true, default-features = true }
indexmap = { workspace = true, default-features = true }
rustc-hash = { workspace = true, default-features = true }
futures = { workspace = true, default-features = true }
parking_lot = { workspace = true, default-features = true }
itertools = { workspace = true, default-features = true }
send_wrapper = { workspace = true, default-features = true }
linear-map = "1.2"
sledgehammer_bindgen = { version = "0.6.0", features = [
linear-map = { workspace = true, default-features = true }
sledgehammer_bindgen = { features = [
"web",
], optional = true }
sledgehammer_utils = { version = "0.3.1", optional = true }
], optional = true, workspace = true, default-features = true }
sledgehammer_utils = { optional = true, workspace = true, default-features = true }
tracing = { optional = true, workspace = true, default-features = true }
serde = { optional = true, workspace = true, default-features = true }
serde_json = { optional = true, workspace = true, default-features = true }

View File

@@ -540,11 +540,20 @@ impl IntoClass for (&'static str, bool) {
fn rebuild(self, state: &mut Self::State) {
let (name, include) = self;
let (class_list, prev_include, prev_name) = state;
if include != *prev_include {
if name == *prev_name {
if include != *prev_include {
if include {
Rndr::add_class(class_list, name);
} else {
Rndr::remove_class(class_list, name);
}
}
} else {
if *prev_include {
Rndr::remove_class(class_list, prev_name);
}
if include {
Rndr::add_class(class_list, name);
} else {
Rndr::remove_class(class_list, name);
}
}
*prev_include = include;

View File

@@ -709,7 +709,7 @@ where
buf.push('<');
buf.push_str(E::TAG);
<At as ToTemplate>::to_template(
<At as ToTemplate>::to_template_attribute(
buf,
&mut class,
&mut style,

View File

@@ -125,14 +125,14 @@ impl Render for InertElement {
type State = InertElementState;
fn build(self) -> Self::State {
let el = Rndr::create_element_from_html(&self.html);
let el = Rndr::create_element_from_html(self.html.clone());
InertElementState(self.html, el)
}
fn rebuild(self, state: &mut Self::State) {
let InertElementState(prev, el) = state;
if &self.html != prev {
let mut new_el = Rndr::create_element_from_html(&self.html);
let mut new_el = Rndr::create_element_from_html(self.html.clone());
el.insert_before_this(&mut new_el);
el.unmount();
*el = new_el;

View File

@@ -209,6 +209,15 @@ where
fn rebuild(self, state: &mut Self::State) {
let (name, mut f) = self;
let prev_name = state.name;
let prev_state = state.effect.take_value();
if let Some((list, prev_include)) = &prev_state {
if prev_name != name && *prev_include {
Rndr::remove_class(list, prev_name);
}
}
// Name might've updated:
state.name = name;
let mut first_run = true;
@@ -232,7 +241,7 @@ where
}
}
},
state.effect.take_value(),
prev_state,
);
}

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