Compare commits

...

131 Commits
3890 ... 4088

Author SHA1 Message Date
Greg Johnston
54764261b1 fix memory leak! 2025-06-25 10:02:41 -04:00
Greg Johnston
acd8fd52c3 fix ownership for root 2025-06-24 22:01:26 -04:00
Greg Johnston
51bd63a91a fix context providers (for <A> etc) 2025-06-23 21:50:21 -04:00
Greg Johnston
1d34ea7133 Merge remote-tracking branch 'origin' into 4088 2025-06-23 20:05:40 -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
Greg Johnston
a8894460e2 Merge remote-tracking branch 'origin' into 4088 2025-06-20 20:40:11 -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
Greg Johnston
403d6ed84e chore: clippy 2025-06-20 17:42:56 -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
b37900ec55 update rebuild 2025-06-19 14:40:19 -04:00
Greg Johnston
fda2a1348c fix overwritten Outlet 2025-06-19 14:23:08 -04:00
Greg Johnston
c2ea59ca96 correct top-level Outlet rendering again 2025-06-18 15:38:09 -04:00
Greg Johnston
7af2a31e21 waypoint 2025-06-18 15:24:03 -04:00
Greg Johnston
0e45694c6e correctly render top-level Outlet once 2025-06-17 19:44:42 -04:00
Greg Johnston
ca5c179cb1 fix: correctly provide context via nested outlets (closes #4088) 2025-06-16 21:12:42 -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
Greg Johnston
f4e0be2d59 fix: do not track anything inside the async block of a Resource (closes #4060) (#4061) 2025-06-10 21:11:29 -04:00
Josiah Parry
05f50f7d27 chore: fix checkboxes in issue template (#4031) 2025-06-08 20:55:16 -04:00
Greg Johnston
a22d6f58be fix: allow nested untracked reads without false positives (closes #4032) (#4049) 2025-06-08 20:54:37 -04:00
Greg Johnston
ff21c9cae2 fix: suppress false-positive warning when adding children to a <For/> that is not currently mounted (closes #3385) (#4050)
* fix: suppress false-positive warning when adding children to a `<For/>` that is not currently mounted (closes #3385)

* remove track_caller
2025-06-08 20:54:25 -04: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
Greg Johnston
733a353820 fix: allow multiple #[middleware] macros (closes #4029) (#4048) 2025-06-06 20:50:52 -04:00
Greg Johnston
829b07b598 Merge pull request #4033 from leptos-rs/update_session_auth
Update `session_auth_axum` example
2025-06-02 19:44:21 -04: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
Greg Johnston
0df6cd74ee feat: simplify session_auth_axumby removing custom handlers 2025-05-30 18:32:22 -04:00
Greg Johnston
1da833a0aa fix: update session_auth_axum to Axum 0.8 2025-05-30 17:30:09 -04:00
Saikat Das
f37d124d6a Fix typo (#4025) 2025-05-29 12:39:32 -07:00
benwis
5d0e683b0f Update tachys to v0.2.3 2025-05-29 12:37:21 -07:00
lcnr
f34e3a5bc9 remove unnecessary where-clauses (#4023)
they may cause tachys to break with -Znext-solver
2025-05-29 12:35:50 -07:00
martin frances
d7dd6a1109 chore: bump rkyv to 0.8.10. (#4018) 2025-05-27 21:15:59 -04:00
Greg Johnston
ff81d34084 fix: fix <select> value by ensuring HTML children are mounted before building attributes (closes #4005) (#4008) 2025-05-27 21:15:41 -04:00
Soso
40a7aba3bc feat: impl AttributeValue for Cow<'_, str> (#4013) 2025-05-27 21:15:12 -04:00
Greg Johnston
d4dcafd908 Merge pull request #4015 from leptos-rs/3042v2
Fix context issues with nesting routing
2025-05-27 21:12:58 -04:00
Greg Johnston
82ccbbf806 copy-paste errors 2025-05-25 15:32:33 -04:00
Greg Johnston
5ba45bb1ed fix: allow Outlet to access context provided in parent view (closes #3042) 2025-05-24 17:51:22 -04:00
Greg Johnston
06dfa37eee feat: allow joining two context trees 2025-05-24 17:50:51 -04:00
Nicolas Cura
e82a0bbc7f chore: handle_response_inner public to be used in custom file_and_error_handler (closes #3996) (#3998) 2025-05-23 14:51:08 -04:00
Álvaro Mondéjar Rubio
4a972fc09e Merge pull request #4003 from mondeja/document-prop-attrs
docs: document `#[prop(default = ...)]` and `#[prop(name = ...)]`
2025-05-23 14:49:59 -04:00
Greg Johnston
07cf649e3b Merge pull request #3994 from leptos-rs/3983
fix: don't use `Arc::ptr_eq` for string comparison (closes #3983)
2025-05-19 19:15:17 -04:00
Greg Johnston
0e9598b799 fix: don't assume classList is unchanged when rebuilding a class effect for the first time (#3983 part two) 2025-05-19 09:42:33 -04:00
Greg Johnston
82303d7e33 fix: don't use Arc::ptr_eq for string comparison (closes #3983) 2025-05-19 09:27:17 -04:00
Álvaro Mondéjar Rubio
c4354ac965 fix: forward missing lint attributes passed to #[component] macro (#3989) 2025-05-18 20:38:16 -04:00
Dennis Waldherr
7de550685a docs: provide error message if file hashing is enabled but no hash file is present (#3990)
Co-authored-by: Dennis Waldherr <bytekeeper@mailbox.org>
2025-05-18 20:30:53 -04:00
mskorkowski
b1f3f6023e fix: smooshed static segments no longer matches the path #3968 (#3973)
* fix: smooshed static segments no longer matches the path #3968

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-05-18 20:29:40 -04:00
Greg Johnston
c189c3a45d fix: meta tags not properly rendered inside synchronously-available Suspend (closes #3976) (#3991) 2025-05-18 20:29:14 -04:00
Eric Roman
3903867f82 Fix spelling typos. (#3965) 2025-05-17 08:49:57 +02:00
Greg Johnston
a42fa452fc feat: add missing Resource::write() and similar functions (see #3959) (#3984) 2025-05-16 09:23:28 -04:00
Saber Haj Rabiee
cd48a6ac8c fix: remove non-existent feature dep in leptos_macro (#3985) 2025-05-16 09:23:04 -04:00
Greg Johnston
34c14adcb8 fix: render identical branch structure for out-of-order and async streaming of Suspense (closes #3970) (#3977) 2025-05-15 19:44:46 -04:00
Saber Haj Rabiee
50cee1d614 chore: upgrade rand and getrandom (#3840)
* chore: update rand and getrandom

* fix: use rng instead of thread_rng

* fix: enable getrandom wasm js backend in build.rs
2025-05-15 11:17:32 +02:00
Saber Haj Rabiee
7ca691305f chore: unify all deps with min occurrences of 2 (#3854) 2025-05-14 20:34:33 -04:00
Greg Johnston
830882f330 fix: allow rebuilding Vec<_> before it is mounted (closes #3962) (#3966) 2025-05-12 15:26:05 -04:00
Scott Little
13110a35e2 fix: add namespace to g in svg portals (closes #3958) (#3960) 2025-05-09 16:44:40 -04:00
Marcus Whybrow
304dc081a2 fix: correct doc comment for SsrMode::PartiallyBlocked (closes #3963) (#3964) 2025-05-09 09:39:00 -07:00
Serhii Shliakhov
14f6bc658e fix: deprecated parameters js warning (#3956) 2025-05-09 08:04:15 -04:00
Eric Roman
09894aaca9 Remove unnecessary "crate::" prefix in a documentation example. (#3952) 2025-05-08 07:43:27 -07:00
Greg Johnston
2ee4444bb4 v0.8.2 2025-05-06 14:09:35 -04:00
Luxalpa
03a1c1e7a6 fix: ensure unique style caching hashes (#3947) 2025-05-06 14:00:29 -04:00
Greg Johnston
12e49ed996 Merge pull request #3950 from leptos-rs/3945
fix: correct order of meta content relative to surrounding tags (closes #3945)
2025-05-06 13:59:28 -04:00
Greg Johnston
1e281e9e74 fix(examples): bugfix revealed a pre-existing bug with meta tags in the hackernews demo! 2025-05-06 11:49:43 -04:00
Greg Johnston
bd475f89d0 fix: correct order of meta content relative to surrounding tags (closes #3945) 2025-05-06 11:19:19 -04:00
Greg Johnston
3d91b5e90f v0.8.1 2025-05-05 21:39:43 -04:00
Greg Johnston
96d8d5218c Merge pull request #3942 from leptos-rs/3907
Some `islands_router` improvements
2025-05-05 21:33:57 -04:00
Greg Johnston
84caa35cef feat: add .map() and .and_then() on LocalResource (#3941) 2025-05-05 21:20:34 -04:00
Greg Johnston
fc8b55161c fix: remove extra marker node after text node when marking a branch (closes #3936) (#3940) 2025-05-05 21:20:16 -04:00
Greg Johnston
657052466b fix: use a runtime check rather than an unnecessary Either to determine how to render islands (see #3896; closes #3929) (#3938) 2025-05-05 19:41:29 -04:00
william light
efe8336363 reactive_stores: implement PartialEq and Eq for Store (#3915)
StoredValue also has these implemented and does the same thing.
2025-05-05 14:32:42 -04:00
Greg Johnston
770881842c fix: correctly provide context through islands to children (closes #3928) (#3933) 2025-05-05 13:00:40 -04:00
Greg Johnston
0d540ef02f fix: ensure that nested children of a RenderEffect are dropped while dropped a RenderEffect (closes #3922) (#3926) 2025-05-05 13:00:20 -04:00
Saber Haj Rabiee
dc1885ad92 feat: check the counter_isomorphic release build with the leptos_debuginfo flag (#3918) 2025-05-04 15:22:04 -04:00
Eric Roman
61bf87439a Fix some typos in the documentation/examples for reactive store. (#3924) 2025-05-03 20:50:13 -04:00
Greg Johnston
308568e520 fix(CI): prevent regreession from nightly clippy in autofix (#3917)
* fix(CI): prevent regreession from nightly clippy in autofix

* chore: format

* chore: update nightly to 2025-04-16 (proc-macro span, #3852)

* chore: improve the autofix ci workflow

* fix: adjust ServerFn macro test stderr based on nightly-2025-04-16

* fix: limit server_fn server macro trybuild tests nightly only
2025-05-03 20:47:42 -04:00
Greg Johnston
1b0f32dc4c fix: clear and re-throw errors in correct order (#3923) 2025-05-03 20:46:57 -04:00
Greg Johnston
2e0b3011d9 fix: correct issues with StaticVec::rebuild() by aligning implementation with Vec::rebuild() (closes #3906) (#3920) 2025-05-03 08:56:36 -04:00
Greg Johnston
680d4ccd07 fix: do not diff islands in islands_router mode (see #3907) 2025-05-02 21:20:16 -04:00
Greg Johnston
325f9cbe33 fix: don't handle ActionForm (et al) with the islands_router default <form> behavior (see #3907) 2025-05-02 21:12:20 -04:00
Greg Johnston
26ab392c95 fix: allow nested Suspense > ErrorBoundary > Suspense (closes #3908) (#3913) 2025-05-02 16:59:04 -04:00
Saber Haj Rabiee
3a4e2a19aa fix: limit server_fn server macro trybuild tests nightly only 2025-05-02 08:32:38 -07:00
Saber Haj Rabiee
eed3d21b40 fix: adjust ServerFn macro test stderr based on nightly-2025-04-16 2025-05-02 08:01:15 -07:00
Saber Haj Rabiee
7ae386285d chore: improve the autofix ci workflow 2025-05-02 07:27:45 -07:00
Saber Haj Rabiee
ebcc51136d chore: update nightly to 2025-04-16 (proc-macro span, #3852) 2025-05-02 07:13:34 -07:00
Saber Haj Rabiee
e10ded4fd0 chore: format 2025-05-02 06:58:01 -07:00
Saber Haj Rabiee
67be872f58 fix(CI): prevent regreession from nightly clippy in autofix 2025-05-02 06:31:39 -07:00
LeoniePhiline
e5b21ac0fc fix(docs): correct panic message in copied example code (#3911) 2025-05-02 08:14:36 -04:00
benwis
9b2e313d20 v0.8.0 2025-05-01 15:26:21 -07:00
Greg Johnston
bc79232033 feat: impl From<MappedSignal<T>> for Signal<T> (#3897) 2025-05-01 14:56:27 -04:00
Saber Haj Rabiee
a7bb2565c4 fix(examples): websocket tests fail (occasionally) second attemp (#3910)
do not check the label immediately
2025-05-01 14:54:57 -04:00
zakstucke
2e393aaca0 fix: leptos_debuginfo (#3899) 2025-05-01 14:54:24 -04:00
Greg Johnston
e37711cb85 fix: correct hydration for elements after island children (closes #3904) (#3905) 2025-05-01 09:44:40 -04:00
Greg Johnston
627b553e60 fix: prevent sibling context leakage in islands (closes #3902) (#3903) 2025-04-30 21:33:54 -04:00
Greg Johnston
e2f9aca466 Merge pull request #3901 from leptos-rs/3896
Fix some island-routing issues
2025-04-30 14:27:31 -04:00
Greg Johnston
970544ed0b fix: correctly hydrate branches inside islands when using islands-router (closes #3896) 2025-04-30 08:48:40 -04:00
Greg Johnston
0c3b3c440f chore: remove forgotten log 2025-04-30 08:36:23 -04:00
nickburlett
6578086e09 fix(examples): incorrect routes in hackernews example (closes #3892) (#3894)
* fix(examples): incorrect routes in hackernews example (closes #3892)

1. `Avoid calling category()` twice on the story type.
2. `get_static_file()` returns Err on not found, so don’t
   unconditionally `unwrap()` it

* cargo fmt

---------

Co-authored-by: Greg Johnston <greg.johnston@gmail.com>
2025-04-30 07:43:12 -04:00
Greg Johnston
113aba9666 docs: add note about file hashing in Stylesheet docs (#3898) 2025-04-29 19:03:18 -07:00
Greg Johnston
58d7475193 fix: don't render branching comments inside script/style tags 2025-04-29 20:32:56 -04:00
Greg Johnston
04d80ff8d0 chore: clippy 2025-04-29 20:31:44 -04:00
Greg Johnston
7971a2dccb fix: marking AnyView branches with out-of-order streaming (for islands_router) 2025-04-29 20:30:40 -04:00
Greg Johnston
e2ea4277bc fix(examples): broken favicons in hackernews examples (closes #3890) (#3891) 2025-04-28 09:52:15 -04:00
156 changed files with 3429 additions and 2009 deletions

View File

@@ -33,10 +33,11 @@ Steps to reproduce the behavior:
If applicable, add screenshots to help explain your problem.
**Next Steps**
[ ] I will make a PR
[ ] I would like to make a PR, but need help getting started
[ ] I want someone else to take the time to fix this
[ ] This is a low priority for me and is just shared for your information
- [ ] I will make a PR
- [ ] I would like to make a PR, but need help getting started
- [ ] I want someone else to take the time to fix this
- [ ] This is a low priority for me and is just shared for your information
**Additional context**
Add any other context about the problem here.

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

@@ -21,34 +21,20 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: actions-rust-lang/setup-rust-toolchain@v1
with: {toolchain: nightly, components: "rustfmt, clippy", target: "wasm32-unknown-unknown", rustflags: ""}
with: {toolchain: "nightly-2025-04-16", components: "rustfmt, clippy", target: "wasm32-unknown-unknown", rustflags: ""}
- name: Install Glib
run: |
sudo apt-get update
sudo apt-get install -y libglib2.0-dev
- name: Install cargo-all-features
run: cargo install --git https://github.com/sabify/cargo-all-features --branch arbitrary-command-support
- name: Install jq
run: sudo apt-get install jq
- run: |
echo "Formatting the workspace"
cargo fmt --all
echo "Running Clippy against each member's features (default features included)"
for member in $(cargo metadata --no-deps --format-version 1 | jq -r '.packages[] | .name'); do
echo "Working on member $member":
echo -e "\tdefault-features/no-features:"
# this will also run on members with no features or default features
cargo clippy --allow-dirty --fix --lib --package "$member"
features=$(cargo metadata --no-deps --format-version 1 | jq -r ".packages[] | select(.name == \"$member\") | .features | keys[]")
for feature in $features; do
if [ "$feature" = "default" ]; then
continue
fi
echo -e "\tfeature $feature"
cargo clippy --allow-dirty --fix --lib --package "$member" --features "$feature"
done
done
- uses: autofix-ci/action@v1.3.1
- name: Format the workspace
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.2
if: ${{ always() }}
with:
fail-fast: false

View File

@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
toolchain: [stable, nightly-2025-03-05]
toolchain: [stable, nightly-2025-04-16]
erased_mode: [true, false]
steps:
- name: Free Disk Space
@@ -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,16 @@ 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')
run: |
cd '${{ inputs.directory }}'
RUSTFLAGS="$RUSTFLAGS --cfg leptos_debuginfo" cargo leptos build --release
- name: Clean up ${{ inputs.directory }}
if: always()
run: |

943
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -40,44 +40,131 @@ members = [
exclude = ["benchmarks", "examples", "projects"]
[workspace.package]
version = "0.8.0-rc3"
version = "0.8.2"
edition = "2021"
rust-version = "1.76"
rust-version = "1.80"
[workspace.dependencies]
convert_case = "0.8"
# members
throw_error = { path = "./any_error/", version = "0.3.0" }
any_spawner = { path = "./any_spawner/", version = "0.3.0-rc3" }
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" }
itertools = "0.14.0"
leptos = { path = "./leptos", version = "0.8.0-rc3" }
leptos_config = { path = "./leptos_config", version = "0.8.0-rc3" }
leptos_dom = { path = "./leptos_dom", version = "0.8.0-rc3" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.0-rc3" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.8.0-rc3" }
leptos_macro = { path = "./leptos_macro", version = "0.8.0-rc3" }
leptos_router = { path = "./router", version = "0.8.0-rc3" }
leptos_router_macro = { path = "./router_macro", version = "0.8.0-rc3" }
leptos_server = { path = "./leptos_server", version = "0.8.0-rc3" }
leptos_meta = { path = "./meta", version = "0.8.0-rc3" }
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" }
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-rc3" }
reactive_stores = { path = "./reactive_stores", version = "0.2.0-rc3" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.2.0-rc3" }
rustversion = "1"
serde_json = "1.0.0"
server_fn = { path = "./server_fn", version = "0.8.0-rc3" }
server_fn_macro = { path = "./server_fn_macro", version = "0.8.0-rc3" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.0-rc3" }
tachys = { path = "./tachys", version = "0.2.0-rc3" }
trybuild = "1"
typed-builder = "0.21.0"
thiserror = "2.0.12"
wasm-bindgen = "0.2.100"
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" }
tachys = { path = "./tachys", version = "0.2.0" }
# 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.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.12" }
wasm-bindgen = { default-features = false, version = "0.2.100" }
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.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.7" }
futures = { default-features = false, version = "0.3.31" }
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.1" }
const_format = { default-features = false, version = "0.2.34" }
gloo-net = { default-features = false, version = "0.6.0" }
url = { default-features = false, version = "2.5.4" }
tokio = { default-features = false, version = "1.45.1" }
base64 = { default-features = false, version = "0.22.1" }
cfg-if = { default-features = false, version = "1.0.0" }
wasm-bindgen-futures = { default-features = false, version = "0.4.50" }
tower = { default-features = false, version = "0.5.2" }
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.101" }
xxhash-rust = { default-features = false, version = "0.8.15" }
paste = { default-features = false, version = "1.0.15" }
quote = { default-features = false, version = "1.0.40" }
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.26.2" }
serial_test = { default-features = false, version = "3.2.0" }
erased = { default-features = false, version = "0.1.2" }
glib = { default-features = false, version = "0.20.10" }
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.33" }
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

@@ -10,4 +10,4 @@ rust-version.workspace = true
edition.workspace = true
[dependencies]
pin-project-lite = "0.2.15"
pin-project-lite = { workspace = true, default-features = true }

View File

@@ -1,6 +1,6 @@
[package]
name = "any_spawner"
version = "0.3.0-rc3"
version = "0.3.0"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
@@ -9,25 +9,25 @@ description = "Spawn asynchronous tasks in an executor-independent way."
edition.workspace = true
[dependencies]
async-executor = { version = "1.13.1", optional = true }
futures = "0.3.31"
glib = { version = "0.20.6", optional = true }
thiserror = { workspace = true }
tokio = { version = "1.41", optional = true, default-features = false, features = [
async-executor = { optional = true , workspace = true, default-features = true }
futures = { workspace = true, default-features = true }
glib = { optional = true , workspace = true, default-features = true }
thiserror = { workspace = true , default-features = true }
tokio = { optional = true, default-features = false, features = [
"rt",
] }
tracing = { version = "0.1.41", optional = true }
wasm-bindgen-futures = { version = "0.4.50", optional = true }
] , workspace = true }
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 }
tokio = { version = "1.41", default-features = false, features = [
futures-lite = { default-features = false , workspace = true }
tokio = { default-features = false, features = [
"rt",
"macros",
"time",
] }
wasm-bindgen-test = { version = "0.3.50" }
serial_test = "3.2.0"
] , workspace = true }
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

@@ -31,7 +31,7 @@ pub const fn const_concat(
let mut i = 0;
// have it iterate over bytes manually, because, again,
// no mutable refernces in const fns
// no mutable references in const fns
while i < x.len() {
buffer[position] = x[i];
position += 1;
@@ -59,7 +59,7 @@ pub const fn const_concat_with_prefix(
let mut i = 0;
// have it iterate over bytes manually, because, again,
// no mutable refernces in const fns
// no mutable references in const fns
while i < x.len() {
buffer[position] = x[i];
position += 1;
@@ -116,7 +116,7 @@ pub const fn const_concat_with_separator(
let mut i = 0;
// have it iterate over bytes manually, because, again,
// no mutable refernces in const fns
// no mutable references in const fns
while i < x.len() {
buffer[position] = x[i];
position += 1;

View File

@@ -10,8 +10,8 @@ rust-version.workspace = true
edition.workspace = true
[dependencies]
pin-project-lite = "0.2.16"
paste = "1.0.15"
pin-project-lite = { workspace = true, default-features = true }
paste = { workspace = true, default-features = true }
[features]
default = ["no_std"]

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

@@ -7,7 +7,7 @@ pub fn main() {
fmt()
.with_writer(
// To avoide trace events in the browser from showing their
// To avoid trace events in the browser from showing their
// JS backtrace, which is very annoying, in my opinion
MakeConsoleWriter::default()
.map_trace_level_to(tracing::Level::DEBUG),

View File

@@ -6,7 +6,7 @@ use leptos_router::{components::A, hooks::use_params_map};
#[component]
pub fn Story() -> impl IntoView {
let params = use_params_map();
let story = Resource::new(
let story = Resource::new_blocking(
move || params.read().get("id").unwrap_or_default(),
move |id| async move {
if id.is_empty() {

View File

@@ -1,7 +1,7 @@
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use axum::Router;
use axum::{routing::get, Router};
use hackernews_axum::{shell, App};
use leptos::config::get_configuration;
use leptos_axum::{generate_route_list, LeptosRoutes};
@@ -13,6 +13,15 @@ async fn main() {
// build our application with a route
let app = Router::new()
.route(
"/favicon.ico",
get(|| async {
(
[("content-type", "image/x-icon")],
include_bytes!("../public/favicon.ico"),
)
}),
)
.leptos_routes(&leptos_options, routes, {
let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone())

View File

@@ -6,7 +6,7 @@ use leptos_router::{components::A, hooks::use_params_map};
#[component]
pub fn Story() -> impl IntoView {
let params = use_params_map();
let story = Resource::new(
let story = Resource::new_blocking(
move || params.read().get("id").unwrap_or_default(),
move |id| async move {
if id.is_empty() {

View File

@@ -26,12 +26,17 @@ pub async fn file_and_error_handler(
.map(|h| h.to_str().unwrap_or("none"))
.unwrap_or("none")
.to_string();
let res = get_static_file(uri.clone(), accept_encoding).await.unwrap();
let static_result = get_static_file(uri.clone(), accept_encoding).await;
if res.status() == StatusCode::OK {
res.into_response()
} else {
(StatusCode::NOT_FOUND, "Not found.").into_response()
match static_result {
Ok(res) => {
if res.status() == StatusCode::OK {
res.into_response()
} else {
(StatusCode::NOT_FOUND, "Not found.").into_response()
}
}
Err(e) => e.into_response(),
}
}

View File

@@ -1,6 +1,7 @@
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use axum::routing::get;
pub use axum::Router;
use hackernews_islands::*;
pub use leptos::config::get_configuration;
@@ -25,6 +26,7 @@ async fn main() {
// build our application with a route
let app = Router::new()
.route("/favicon.ico", get(fallback::file_and_error_handler))
.leptos_routes(&leptos_options, routes, {
let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone())

View File

@@ -47,7 +47,7 @@ pub fn Stories() -> impl IntoView {
let stories = Resource::new(
move || (page(), story_type()),
move |(page, story_type)| async move {
fetch_stories(category(&story_type), page).await.ok()
fetch_stories(story_type, page).await.ok()
},
);
let (pending, set_pending) = signal(false);

View File

@@ -13,7 +13,7 @@ pub async fn fetch_story(
#[component]
pub fn Story() -> impl IntoView {
let params = use_params_map();
let story = Resource::new(
let story = Resource::new_blocking(
move || params.read().get("id").unwrap_or_default(),
move |id| async move {
if id.is_empty() {

View File

@@ -7,7 +7,7 @@ use send_wrapper::SendWrapper;
#[component]
pub fn Story() -> impl IntoView {
let params = use_params_map();
let story = Resource::new(
let story = Resource::new_blocking(
move || params.read().get("id").unwrap_or_default(),
move |id| {
SendWrapper::new(async move {

View File

@@ -10,7 +10,11 @@ crate-type = ["cdylib", "rlib"]
console_error_panic_hook = "0.1.7"
futures = "0.3.30"
http = "1.1"
leptos = { path = "../../leptos", features = ["tracing", "islands"] }
leptos = { path = "../../leptos", features = [
"tracing",
"islands",
"islands-router",
] }
leptos_router = { path = "../../router" }
server_fn = { path = "../../server_fn", features = ["serde-lite"] }
leptos_axum = { path = "../../integrations/axum", features = [

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

@@ -1,6 +1,6 @@
# Leptos Counter Example
# Stores Example
This example creates a simple counter in a client side rendered app with Rust and WASM!
This example shows how to use reactive stores, by building a client-side rendered TODO application.
## Getting Started

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 =
@@ -561,7 +563,7 @@ fn ShowCounters() -> impl IntoView {
//
// However, upon `Reset Counters`, the mode from which the reset
// was issued will result in the rendering be reflected as such, so
// if the intial state was SSR, resetting under CSR will result in
// if the initial state was SSR, resetting under CSR will result in
// the CSR counters be rendered after. However for the intents and
// purpose for the testing only the CSR is cared for.
//

View File

@@ -10,19 +10,9 @@ pub async fn goto_path(client: &Client, path: &str) -> Result<()> {
Ok(())
}
pub async fn add_text(client: &Client, text: &str) -> Result<String> {
fill_input(client, text).await?;
get_label(client).await
}
pub async fn fill_input(client: &Client, text: &str) -> Result<()> {
let textbox = find::input(client).await;
textbox.send_keys(text).await?;
Ok(())
}
pub async fn get_label(client: &Client) -> Result<String> {
let label = find::label(client).await;
Ok(label.text().await?)
}

View File

@@ -9,13 +9,3 @@ pub async fn input(client: &Client) -> Element {
textbox
}
pub async fn label(client: &Client) -> Element {
let label = client
.wait()
.for_element(Locator::Css("p"))
.await
.expect("");
label
}

View File

@@ -14,7 +14,7 @@ async fn i_open_the_app(world: &mut AppWorld) -> Result<()> {
#[given(regex = "^I add a text as (.*)$")]
async fn i_add_a_text(world: &mut AppWorld, text: String) -> Result<()> {
let client = &world.client;
action::add_text(client, text.as_str()).await?;
action::fill_input(client, text.as_str()).await?;
Ok(())
}

View File

@@ -20,7 +20,7 @@ async fn i_see_the_label_of_the_input_is(
world: &mut AppWorld,
text: String,
) -> Result<()> {
sleep(Duration::from_millis(50)).await;
sleep(Duration::from_millis(500)).await;
let client = &world.client;
check::text_on_element(client, "p", &text).await?;

View File

@@ -12,12 +12,11 @@ edition.workspace = true
[dependencies]
throw_error = { workspace = true }
or_poisoned = { workspace = true }
futures = "0.3.31"
serde = { version = "1.0", features = ["derive"] }
wasm-bindgen = { workspace = true, optional = true }
js-sys = { version = "0.3.74", optional = true }
once_cell = "1.20"
pin-project-lite = "0.2.15"
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 }
pin-project-lite = { workspace = true, default-features = true }
[features]
browser = ["dep:wasm-bindgen", "dep:js-sys"]

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,10 +9,10 @@ rust-version.workspace = true
edition.workspace = true
[dependencies]
actix-http = "3.9"
actix-files = "0.6"
actix-web = "4.9"
futures = "0.3.31"
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"] }
hydration_context = { workspace = true }
leptos = { workspace = true, features = ["nonce", "ssr"] }
@@ -22,13 +22,12 @@ leptos_meta = { workspace = true, features = ["nonce"] }
leptos_router = { workspace = true, features = ["ssr"] }
server_fn = { workspace = true, features = ["actix"] }
tachys = { workspace = true }
serde_json = { workspace = true }
parking_lot = "0.12.3"
tracing = { version = "0.1", optional = true }
tokio = { version = "1.43", features = ["rt", "fs"] }
send_wrapper = "0.6.0"
dashmap = "6"
once_cell = "1"
serde_json = { workspace = true , default-features = true }
parking_lot = { workspace = true, default-features = true }
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 }
[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,18 +4,18 @@ authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
description = "Axum integrations for the Leptos web framework."
version = "0.8.0-rc3"
version = "0.8.2"
rust-version.workspace = true
edition.workspace = true
[dependencies]
any_spawner = { workspace = true, features = ["tokio"] }
hydration_context = { workspace = true }
axum = { version = "0.8.1", default-features = false, features = [
axum = { default-features = false, features = [
"matched-path",
] }
dashmap = "6"
futures = "0.3.31"
] , workspace = true }
dashmap = { workspace = true, default-features = true }
futures = { workspace = true, default-features = true }
leptos = { workspace = true, features = ["nonce", "ssr"] }
server_fn = { workspace = true, features = ["axum-no-default"] }
leptos_macro = { workspace = true, features = ["axum"] }
@@ -23,16 +23,15 @@ leptos_meta = { workspace = true, features = ["ssr", "nonce"] }
leptos_router = { workspace = true, features = ["ssr"] }
leptos_integration_utils = { workspace = true }
tachys = { workspace = true }
once_cell = "1"
parking_lot = "0.12.3"
tokio = { version = "1.43", default-features = false }
tower = { version = "0.5.1", features = ["util"] }
tower-http = "0.6.2"
tracing = { version = "0.1.41", optional = true }
parking_lot = { 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 = "0.8.1"
tokio = { version = "1.43", features = ["net", "rt-multi-thread"] }
axum = { 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;
@@ -590,7 +590,7 @@ where
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
/// to route it using [leptos_router], serving an HTML stream of your application.
///
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network
/// This version allows us to pass Axum State/Extension/Extractor or other info from Axum or network
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
/// the data to leptos in a closure. An example is below
/// ```
@@ -796,7 +796,7 @@ where
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve before
/// sending down its HTML. The app will become interactive once it has fully loaded.
///
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network
/// This version allows us to pass Axum State/Extension/Extractor or other info from Axum or network
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
/// the data to leptos in a closure. An example is below
/// ```
@@ -879,7 +879,8 @@ where
}
}
fn handle_response_inner<IV>(
/// Can be used in conjunction with a custom [file_and_error_handler_with_context] to process an Axum [Request](axum::extract::Request) into an Axum [Response](axum::response::Response)
pub fn handle_response_inner<IV>(
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl FnOnce() -> IV + Send + 'static,
req: Request<Body>,
@@ -1022,7 +1023,7 @@ where
/// to route it using [leptos_router], asynchronously rendering an HTML page after all
/// `async` resources have loaded.
///
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network
/// This version allows us to pass Axum State/Extension/Extractor or other info from Axum or network
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
/// the data to leptos in a closure. An example is below
/// ```
@@ -1089,7 +1090,7 @@ where
/// to route it using [leptos_router], asynchronously rendering an HTML page after all
/// `async` resources have loaded.
///
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network
/// This version allows us to pass Axum State/Extension/Extractor or other info from Axum or network
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
/// the data to leptos in a closure. An example is below
/// ```
@@ -1521,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

@@ -9,7 +9,7 @@ rust-version.workspace = true
edition.workspace = true
[dependencies]
futures = "0.3.31"
futures = { workspace = true, default-features = true }
hydration_context = { workspace = true }
leptos = { workspace = true, features = ["nonce"] }
leptos_meta = { workspace = true, features = ["ssr"] }

View File

@@ -15,8 +15,8 @@ any_spawner = { workspace = true, features = [
"wasm-bindgen",
"futures-executor",
] }
base64 = { version = "0.22.1", optional = true }
cfg-if = "1.0"
base64 = { optional = true, workspace = true, default-features = true }
cfg-if = { workspace = true, default-features = true }
hydration_context = { workspace = true }
either_of = { workspace = true }
leptos_dom = { workspace = true }
@@ -24,38 +24,38 @@ 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 = "1.0"
rand = { version = "0.8.5", optional = true }
# NOTE: While not used directly, `getrandom`'s `js` feature is needed when `rand` is used on WASM to
paste = { workspace = true, default-features = 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.2", optional = true }
getrandom = { optional = true , workspace = true, default-features = true }
reactive_graph = { workspace = true, features = ["serde"] }
rustc-hash = "2.0"
rustc-hash = { workspace = true, default-features = true }
tachys = { workspace = true, features = [
"reactive_graph",
"reactive_stores",
"oco",
] }
thiserror = { workspace = true }
tracing = { version = "0.1.41", optional = true }
typed-builder = { workspace = true }
typed-builder-macro = "0.21.0"
serde = "1.0"
serde_json = { version = "1.0", optional = true }
thiserror = { workspace = true, default-features = true }
tracing = { optional = true, workspace = true, default-features = true }
typed-builder = { workspace = true, default-features = true }
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"] }
web-sys = { version = "0.3.72", features = [
web-sys = { features = [
"ShadowRoot",
"ShadowRootInit",
"ShadowRootMode",
] }
wasm-bindgen = { workspace = true }
serde_qs = "0.14.0"
slotmap = "1.0"
futures = "0.3.31"
send_wrapper = "0.6.0"
], workspace = true, default-features = true }
wasm-bindgen = { workspace = true, default-features = true }
serde_qs = { workspace = true, default-features = true }
slotmap = { workspace = true, default-features = true }
futures = { workspace = true, default-features = true }
send_wrapper = { workspace = true, default-features = true }
[features]
hydration = [
@@ -64,13 +64,13 @@ hydration = [
"hydration_context/browser",
"leptos_dom/hydration",
]
csr = ["leptos_macro/csr", "reactive_graph/effects", "getrandom?/js"]
csr = ["leptos_macro/csr", "reactive_graph/effects", "getrandom?/wasm_js"]
hydrate = [
"leptos_macro/hydrate",
"hydration",
"tachys/hydrate",
"reactive_graph/effects",
"getrandom?/js",
"getrandom?/wasm_js",
]
default-tls = ["server_fn/default-tls"]
rustls = ["server_fn/rustls"]
@@ -99,9 +99,15 @@ trace-component-props = [
"leptos_dom/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 = "0.4.1"
rustc_version = { workspace = true, default-features = true }
# Having an erasure feature rather than normal --cfg erase_components for the proc macro crate is a workaround for this rust issue:
# https://github.com/rust-lang/cargo/issues/4423

View File

@@ -1,8 +1,15 @@
use rustc_version::{version_meta, Channel};
fn main() {
let target = std::env::var("TARGET").unwrap_or_default();
// Set cfg flags depending on release channel
if matches!(version_meta().unwrap().channel, Channel::Nightly) {
println!("cargo:rustc-cfg=rustc_nightly");
}
// Set cfg flag for getrandom wasm_js
if target == "wasm32-unknown-unknown" {
// Set a custom cfg flag for wasm builds
println!("cargo:rustc-cfg=getrandom_backend=\"wasm_js\"");
}
}

View File

@@ -47,7 +47,7 @@ type BoxedChildrenFn = Box<dyn Fn() -> AnyView + Send>;
///
/// Different component types take different types for their `children` prop, some of which cannot
/// be directly constructed. Using `ToChildren` allows the component user to pass children without
/// explicity constructing the correct type.
/// explicitly constructing the correct type.
///
/// ## Examples
///

View File

@@ -255,7 +255,7 @@ where
) -> Result<Self, serde_qs::Error>;
}
/// Errors that can arise when coverting from an HTML event or form into a Rust data type.
/// Errors that can arise when converting from an HTML event or form into a Rust data type.
#[derive(Error, Debug)]
pub enum FromFormDataError {
/// Could not find a `<form>` connected to the event.

View File

@@ -1,5 +1,5 @@
((root, pkg_path, output_name, wasm_output_name) => {
let MOST_RECENT_CHILDREN_CB;
let MOST_RECENT_CHILDREN_CB = [];
function idle(c) {
if ("requestIdleCallback" in window) {
@@ -22,12 +22,18 @@
traverse(child, children);
}
} else {
if(tag === 'leptos-children') {
MOST_RECENT_CHILDREN_CB = node.$$on_hydrate;
if (tag === 'leptos-children') {
MOST_RECENT_CHILDREN_CB.push(node.$$on_hydrate);
for(const child of node.children) {
traverse(child);
};
// un-set the "most recent children"
MOST_RECENT_CHILDREN_CB.pop();
} else {
for(const child of node.children) {
traverse(child);
};
}
for(const child of node.children) {
traverse(child);
};
}
}
}
@@ -37,8 +43,9 @@
function hydrateIsland(el, id, mod) {
const islandFn = mod[id];
if (islandFn) {
if (MOST_RECENT_CHILDREN_CB) {
MOST_RECENT_CHILDREN_CB();
const children_cb = MOST_RECENT_CHILDREN_CB[MOST_RECENT_CHILDREN_CB.length-1];
if (children_cb) {
children_cb();
}
islandFn(el);
} else {
@@ -48,7 +55,7 @@
idle(() => {
import(`${root}/${pkg_path}/${output_name}.js`)
.then(mod => {
mod.default(`${root}/${pkg_path}/${wasm_output_name}.wasm`).then(() => {
mod.default({module_or_path: `${root}/${pkg_path}/${wasm_output_name}.wasm`}).then(() => {
mod.hydrate();
hydrateIslands(document.body, mod);
});

View File

@@ -17,6 +17,10 @@ window.addEventListener("popstate", async (ev) => {
});
window.addEventListener("submit", async (ev) => {
if (ev.defaultPrevented) {
return;
}
const req = submitToReq(ev);
if(!req) {
return;
@@ -194,6 +198,15 @@ function diffRange(oldDocument, oldRoot, newDocument, newRoot, oldEnd, newEnd) {
else if (oldNode.nodeType === Node.TEXT_NODE) {
oldNode.textContent = newNode.textContent;
}
// islands should not be diffed on the client, because we do not want to overwrite client-side state
// but their children should be diffed still, because they could contain new server content
else if (oldNode.nodeType === Node.ELEMENT_NODE && oldNode.tagName === "LEPTOS-ISLAND") {
// TODO: diff the leptos-children
// skip over leptos-island otherwise
oldDocWalker.nextSibling();
newDocWalker.nextSibling();
}
// if it's an element, replace if it's a different tag, or update attributes
else if (oldNode.nodeType === Node.ELEMENT_NODE) {
diffElement(oldNode, newNode);

View File

@@ -83,6 +83,10 @@ pub fn HydrationScripts(
}
}
}
} else {
leptos::logging::error!(
"File hashing is active but no hash file was found"
);
}
} else if std::option_env!("LEPTOS_OUTPUT_NAME").is_none() {
wasm_file_name.push_str("_bg");
@@ -107,21 +111,19 @@ pub fn HydrationScripts(
.unwrap_or_default();
let root = root.unwrap_or_default();
use_context::<IslandsRouterNavigation>().is_none().then(|| {
view! {
<link rel="modulepreload" href=format!("{root}/{pkg_path}/{js_file_name}.js") nonce=nonce.clone()/>
<link
rel="preload"
href=format!("{root}/{pkg_path}/{wasm_file_name}.wasm")
r#as="fetch"
r#type="application/wasm"
crossorigin=nonce.clone().unwrap_or_default()
/>
<script type="module" nonce=nonce>
{format!("{script}({root:?}, {pkg_path:?}, {js_file_name:?}, {wasm_file_name:?});{islands_router}")}
</script>
}
})
view! {
<link rel="modulepreload" href=format!("{root}/{pkg_path}/{js_file_name}.js") nonce=nonce.clone()/>
<link
rel="preload"
href=format!("{root}/{pkg_path}/{wasm_file_name}.wasm")
r#as="fetch"
r#type="application/wasm"
crossorigin=nonce.clone().unwrap_or_default()
/>
<script type="module" nonce=nonce>
{format!("{script}({root:?}, {pkg_path:?}, {js_file_name:?}, {wasm_file_name:?});{islands_router}")}
</script>
}
}
/// If this is provided via context, it means that you are using the islands router and

View File

@@ -7,7 +7,7 @@ ws.onmessage = (ev) => {
let found = false;
document.querySelectorAll("link").forEach((link) => {
if (link.getAttribute('href').includes(msg.css)) {
let newHref = '/' + msg.css + '?version=' + new Date().getMilliseconds();
let newHref = '/' + msg.css + '?version=' + Date.now();
link.setAttribute('href', newHref);
found = true;
}

View File

@@ -4,7 +4,7 @@ use base64::{
engine::{self, general_purpose},
Engine,
};
use rand::{thread_rng, RngCore};
use rand::{rng, RngCore};
use std::{fmt::Display, ops::Deref, sync::Arc};
use tachys::html::attribute::AttributeValue;
@@ -171,9 +171,9 @@ const NONCE_ENGINE: engine::GeneralPurpose =
impl Nonce {
/// Generates a new nonce from 16 bytes (128 bits) of random data.
pub fn new() -> Self {
let mut thread_rng = thread_rng();
let mut rng = rng();
let mut bytes = [0; 16];
thread_rng.fill_bytes(&mut bytes);
rng.fill_bytes(&mut bytes);
Nonce(NONCE_ENGINE.encode(bytes).into())
}
}

View File

@@ -8,7 +8,7 @@ use std::sync::Arc;
///
/// Useful for inserting modals and tooltips outside of a cropping layout.
/// If no mount point is given, the portal is inserted in `document.body`;
/// it is wrapped in a `<div>` unless `is_svg` is `true` in which case it's wrappend in a `<g>`.
/// it is wrapped in a `<div>` unless `is_svg` is `true` in which case it's wrapped in a `<g>`.
/// Setting `use_shadow` to `true` places the element in a shadow root to isolate styles.
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
#[component]
@@ -42,11 +42,15 @@ where
let children = children.into_inner();
Effect::new(move |_| {
let tag = if is_svg { "g" } else { "div" };
let container = document()
.create_element(tag)
.expect("element creation to work");
let container = if is_svg {
document()
.create_element_ns(Some("http://www.w3.org/2000/svg"), "g")
.expect("SVG element creation to work")
} else {
document()
.create_element("div")
.expect("HTML element creation to work")
};
let render_root = if use_shadow {
container

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.8", default-features = false, features = [
config = { default-features = false, features = [
"toml",
"convert-case",
] }
regex = "1.11"
serde = { version = "1.0", features = ["derive", "rc"] }
thiserror = { workspace = true }
typed-builder = { workspace = true }
] , 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 = { version = "1.43", features = ["rt", "macros"] }
tempfile = "3.14"
temp-env = { version = "0.3.6", features = ["async_closure"] }
tokio = { features = ["rt", "macros"] , workspace = true, default-features = true }
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

@@ -12,19 +12,20 @@ edition.workspace = true
tachys = { workspace = true }
reactive_graph = { workspace = true }
or_poisoned = { workspace = true }
js-sys = "0.3.74"
send_wrapper = "0.6.0"
tracing = { version = "0.1.41", optional = true }
wasm-bindgen = { workspace = true }
serde_json = { version = "1.0", optional = true }
serde = { version = "1.0", optional = true }
js-sys = { 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 }
serde_json = { optional = true , workspace = true, default-features = true }
serde = { optional = true , workspace = true, default-features = true }
[dev-dependencies]
leptos = { path = "../leptos" }
[dependencies.web-sys]
version = "0.3.72"
features = ["Location"]
workspace = true
default-features = true
[features]
default = []

View File

@@ -10,19 +10,19 @@ rust-version.workspace = true
edition.workspace = true
[dependencies]
anyhow = "1.0"
serde = { version = "1.0", features = ["derive"] }
syn = { version = "2.0", features = [
anyhow = { workspace = true, default-features = true }
serde = { features = ["derive"] , workspace = true, default-features = true }
syn = { features = [
"full",
"parsing",
"extra-traits",
"visit",
"printing",
] }
quote = "1.0"
rstml = "0.12.0"
proc-macro2 = { version = "1.0", features = ["span-locations", "nightly"] }
parking_lot = "0.12.3"
walkdir = "2.5"
camino = "1.1"
indexmap = "2.6"
] , workspace = true, default-features = true }
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 = { workspace = true, default-features = true }
camino = { workspace = true, default-features = true }
indexmap = { workspace = true, default-features = true }

View File

@@ -13,39 +13,39 @@ edition.workspace = true
proc-macro = true
[dependencies]
attribute-derive = { version = "0.10.3", features = ["syn-full"] }
cfg-if = "1.0"
html-escape = "0.2.13"
itertools = { workspace = true }
prettyplease = "0.2.25"
proc-macro-error2 = { version = "2.0", default-features = false }
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "2.0", features = ["full"] }
rstml = "0.12.0"
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 = { 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 }
syn = { features = ["full"] , workspace = true, default-features = true }
rstml = { workspace = true, default-features = true }
leptos_hot_reload = { workspace = true }
server_fn_macro = { workspace = true }
convert_case = { workspace = true }
uuid = { version = "1.11", features = ["v4"] }
tracing = { version = "0.1.41", optional = true }
convert_case = { workspace = true , default-features = true }
uuid = { features = ["v4"] , workspace = true, default-features = true }
tracing = { optional = true , workspace = true, default-features = true }
[dev-dependencies]
log = "0.4.22"
typed-builder = "0.20.0"
trybuild = { workspace = true }
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.41"
serde = "1.0"
insta = { workspace = true, default-features = true }
serde = { workspace = true, default-features = true }
[build-dependencies]
rustc_version = "0.4.1"
rustc_version = { workspace = true, default-features = true }
[features]
csr = []
hydrate = []
ssr = ["server_fn_macro/ssr", "leptos/ssr"]
ssr = ["server_fn_macro/ssr"]
nightly = ["server_fn_macro/nightly"]
tracing = ["dep:tracing"]
islands = []

View File

@@ -359,23 +359,11 @@ impl ToTokens for Model {
let component = if is_island {
let hydrate_fn_name = hydrate_fn_name.as_ref().unwrap();
quote! {
{
if ::leptos::context::use_context::<::leptos::reactive::owner::IsHydrating>()
.map(|h| h.0)
.unwrap_or(false) {
::leptos::either::Either::Left(
#component
)
} else {
::leptos::either::Either::Right(
::leptos::tachys::html::islands::Island::new(
stringify!(#hydrate_fn_name),
#component
)
#island_serialized_props
)
}
}
::leptos::tachys::html::islands::Island::new(
stringify!(#hydrate_fn_name),
#component
)
#island_serialized_props
}
} else {
component
@@ -646,7 +634,13 @@ impl Parse for DummyModel {
let mut attrs = input.call(Attribute::parse_outer)?;
// Drop unknown attributes like #[deprecated]
drain_filter(&mut attrs, |attr| {
!(attr.path().is_ident("doc") || attr.path().is_ident("allow"))
let path = attr.path();
!(path.is_ident("doc")
|| path.is_ident("allow")
|| path.is_ident("expect")
|| path.is_ident("warn")
|| path.is_ident("deny")
|| path.is_ident("forbid"))
});
let vis: Visibility = input.parse()?;
@@ -935,13 +929,20 @@ impl UnknownAttrs {
let attrs = attrs
.iter()
.filter_map(|attr| {
if attr.path().is_ident("doc") {
let path = attr.path();
if path.is_ident("doc") {
if let Meta::NameValue(_) = &attr.meta {
return None;
}
}
if attr.path().is_ident("allow") {
if path.is_ident("allow")
|| path.is_ident("expect")
|| path.is_ident("warn")
|| path.is_ident("deny")
|| path.is_ident("forbid")
{
return None;
}

View File

@@ -409,6 +409,7 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
/// generate documentation for the component.
///
/// Heres how you would define and use a simple Leptos component which can accept custom properties for a name and age:
///
/// ```rust
/// # use leptos::prelude::*;
/// use std::time::Duration;
@@ -446,6 +447,7 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
/// ```
///
/// Here are some important details about how Leptos components work within the framework:
///
/// * **The component function only runs once.** Your component function is not a “render” function
/// that re-runs whenever changes happen in the state. Its a “setup” function that runs once to
/// create the user interface, and sets up a reactive system to update it. This means its okay
@@ -458,7 +460,6 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
///
/// ```
/// # use leptos::prelude::*;
///
/// // PascalCase: Generated component will be called MyComponent
/// #[component]
/// fn MyComponent() -> impl IntoView {}
@@ -500,8 +501,10 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
/// ```
///
/// ## Customizing Properties
///
/// You can use the `#[prop]` attribute on individual component properties (function arguments) to
/// customize the types that component property can receive. You can use the following attributes:
///
/// * `#[prop(into)]`: This will call `.into()` on any value passed into the component prop. (For example,
/// you could apply `#[prop(into)]` to a prop that takes
/// [Signal](https://docs.rs/leptos/latest/leptos/struct.Signal.html), which would
@@ -514,6 +517,11 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
/// * `#[prop(optional_no_strip)]`: The same as `optional`, but requires values to be passed as `None` or
/// `Some(T)` explicitly. This means that the optional property can be omitted (and be `None`), or explicitly
/// specified as either `None` or `Some(T)`.
/// * `#[prop(default = <expr>)]`: Optional property that specifies a default value, which is used when the
/// property is not specified.
/// * `#[prop(name = "new_name")]`: Specifiy a different name for the property. Can be used to destructure
/// fields in component function parameters (see example below).
///
/// ```rust
/// # use leptos::prelude::*;
///
@@ -522,6 +530,8 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
/// #[prop(into)] name: String,
/// #[prop(optional)] optional_value: Option<i32>,
/// #[prop(optional_no_strip)] optional_no_strip: Option<i32>,
/// #[prop(default = 7)] optional_default: i32,
/// #[prop(name = "data")] UserInfo { email, user_id }: UserInfo,
/// ) -> impl IntoView {
/// // whatever UI you need
/// }
@@ -530,16 +540,24 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
/// pub fn App() -> impl IntoView {
/// view! {
/// <MyComponent
/// name="Greg" // automatically converted to String with `.into()`
/// optional_value=42 // received as `Some(42)`
/// optional_no_strip=Some(42) // received as `Some(42)`
/// name="Greg" // automatically converted to String with `.into()`
/// optional_value=42 // received as `Some(42)`
/// optional_no_strip=Some(42) // received as `Some(42)`
/// optional_default=42 // received as `42`
/// data=UserInfo {email: "foo", user_id: "bar" }
/// />
/// <MyComponent
/// name="Bob" // automatically converted to String with `.into()`
/// // optional values can both be omitted, and received as `None`
/// data=UserInfo {email: "foo", user_id: "bar" }
/// // optional values can be omitted
/// />
/// }
/// }
///
/// pub struct UserInfo {
/// pub email: &'static str,
/// pub user_id: &'static str,
/// }
/// ```
#[proc_macro_error2::proc_macro_error]
#[proc_macro_attribute]

View File

@@ -112,9 +112,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 +300,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 +396,114 @@ fn inert_element_to_tokens(
html.finish();
Some(quote! {
quote! {
::leptos::tachys::html::InertElement::new(#html)
})
}
}
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 +536,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 +588,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 +702,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) {
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

@@ -10,24 +10,24 @@ rust-version.workspace = true
edition.workspace = true
[dependencies]
base64 = "0.22.1"
codee = { version = "0.3.0", features = ["json_serde"] }
base64 = { workspace = true, default-features = true }
codee = { features = ["json_serde"] , workspace = true, default-features = true }
hydration_context = { workspace = true }
reactive_graph = { workspace = true, features = ["hydration"] }
server_fn = { workspace = true }
tracing = { version = "0.1.41", optional = true }
futures = "0.3.31"
tracing = { optional = true , workspace = true, default-features = true }
futures = { workspace = true, default-features = true }
any_spawner = { workspace = true }
or_poisoned = { workspace = true }
tachys = { workspace = true, optional = true, features = ["reactive_graph"] }
send_wrapper = "0.6"
send_wrapper = { workspace = true, default-features = true }
# serialization formats
serde = { version = "1.0" }
js-sys = { version = "0.3.74", optional = true }
wasm-bindgen = { workspace = true, optional = true }
serde_json = { workspace = true }
serde = { workspace = true, default-features = true }
js-sys = { optional = true , workspace = true, default-features = true }
wasm-bindgen = { workspace = true, optional = true , default-features = true }
serde_json = { workspace = true , default-features = true }
[features]
ssr = []

View File

@@ -14,11 +14,13 @@ use reactive_graph::{
ArcRwSignal, RwSignal,
},
traits::{
DefinedAt, IsDisposed, ReadUntracked, Track, Update, With, Write,
DefinedAt, IsDisposed, Notify, ReadUntracked, Track, UntrackableGuard,
Update, With, Write,
},
};
use std::{
future::{pending, Future, IntoFuture},
ops::{Deref, DerefMut},
panic::Location,
};
@@ -41,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.
///
@@ -62,7 +72,7 @@ impl<T> ArcLocalResource<T> {
pending().await
} else {
// LocalResources that are immediately available can cause a hydration error,
// because the future *looks* like it is alredy ready (and therefore would
// because the future *looks* like it is already ready (and therefore would
// already have been rendered to html on the server), but in fact was ignored
// on the server. the simplest way to avoid this is to ensure that we always
// wait a tick before resolving any value for a localresource.
@@ -157,6 +167,32 @@ impl<T> DefinedAt for ArcLocalResource<T> {
}
}
impl<T> Notify for ArcLocalResource<T>
where
T: 'static,
{
fn notify(&self) {
self.data.notify()
}
}
impl<T> Write for ArcLocalResource<T>
where
T: 'static,
{
type Value = Option<T>;
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
self.data.try_write()
}
fn try_write_untracked(
&self,
) -> Option<impl DerefMut<Target = Self::Value>> {
self.data.try_write_untracked()
}
}
impl<T> ReadUntracked for ArcLocalResource<T>
where
T: 'static,
@@ -241,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
@@ -270,7 +314,7 @@ impl<T> LocalResource<T> {
pending().await
} else {
// LocalResources that are immediately available can cause a hydration error,
// because the future *looks* like it is alredy ready (and therefore would
// because the future *looks* like it is already ready (and therefore would
// already have been rendered to html on the server), but in fact was ignored
// on the server. the simplest way to avoid this is to ensure that we always
// wait a tick before resolving any value for a localresource.
@@ -300,6 +344,34 @@ impl<T> LocalResource<T> {
pub fn refetch(&self) {
self.refetch.try_update(|n| *n += 1);
}
/// Synchronously, reactively reads the current value of the resource and applies the function
/// `f` to its value if it is `Some(_)`.
#[track_caller]
pub fn map<U>(&self, f: impl FnOnce(&T) -> U) -> Option<U>
where
T: 'static,
{
self.data.try_with(|n| n.as_ref().map(f))?
}
}
impl<T, E> LocalResource<Result<T, E>>
where
T: 'static,
E: Clone + 'static,
{
/// Applies the given function when a resource that returns `Result<T, E>`
/// has resolved and loaded an `Ok(_)`, rather than requiring nested `.map()`
/// calls over the `Option<Result<_, _>>` returned by the resource.
///
/// This is useful when used with features like server functions, in conjunction
/// with `<ErrorBoundary/>` and `<Suspense/>`, when these other components are
/// left to handle the `None` and `Err(_)` states.
#[track_caller]
pub fn and_then<U>(&self, f: impl FnOnce(&T) -> U) -> Option<Result<U, E>> {
self.map(|data| data.as_ref().map(f).map_err(|e| e.clone()))
}
}
impl<T> IntoFuture for LocalResource<T>
@@ -336,6 +408,32 @@ impl<T> DefinedAt for LocalResource<T> {
}
}
impl<T> Notify for LocalResource<T>
where
T: 'static,
{
fn notify(&self) {
self.data.notify()
}
}
impl<T> Write for LocalResource<T>
where
T: 'static,
{
type Value = Option<T>;
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
self.data.try_write()
}
fn try_write_untracked(
&self,
) -> Option<impl DerefMut<Target = Self::Value>> {
self.data.try_write_untracked()
}
}
impl<T> ReadUntracked for LocalResource<T>
where
T: 'static,

View File

@@ -26,7 +26,7 @@ use reactive_graph::{
};
use std::{
future::{pending, IntoFuture},
ops::Deref,
ops::{Deref, DerefMut},
panic::Location,
sync::{
atomic::{AtomicBool, Ordering},
@@ -162,6 +162,32 @@ where
}
}
impl<T, Ser> Notify for ArcResource<T, Ser>
where
T: 'static,
{
fn notify(&self) {
self.data.notify()
}
}
impl<T, Ser> Write for ArcResource<T, Ser>
where
T: 'static,
{
type Value = Option<T>;
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
self.data.try_write()
}
fn try_write_untracked(
&self,
) -> Option<impl DerefMut<Target = Self::Value>> {
self.data.try_write_untracked()
}
}
impl<T, Ser> ReadUntracked for ArcResource<T, Ser>
where
T: 'static,
@@ -842,6 +868,32 @@ where
}
}
impl<T, Ser> Notify for Resource<T, Ser>
where
T: Send + Sync + 'static,
{
fn notify(&self) {
self.data.notify()
}
}
impl<T, Ser> Write for Resource<T, Ser>
where
T: Send + Sync + 'static,
{
type Value = Option<T>;
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
self.data.try_write()
}
fn try_write_untracked(
&self,
) -> Option<impl DerefMut<Target = Self::Value>> {
self.data.try_write_untracked()
}
}
impl<T, Ser> ReadUntracked for Resource<T, Ser>
where
T: Send + Sync + 'static,

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_meta"
version = "0.8.0-rc3"
version = "0.8.2"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
@@ -10,17 +10,17 @@ edition.workspace = true
[dependencies]
leptos = { workspace = true }
once_cell = "1.20"
or_poisoned = { workspace = true }
indexmap = "2.6"
send_wrapper = "0.6.0"
tracing = { version = "0.1.41", optional = true }
wasm-bindgen = { workspace = true }
futures = "0.3.31"
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 }
futures = { workspace = true, default-features = true }
[dependencies.web-sys]
version = "0.3.72"
features = ["HtmlLinkElement", "HtmlMetaElement", "HtmlTitleElement"]
workspace = true
default-features = true
[features]
default = []

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,
@@ -216,6 +215,13 @@ impl ServerMetaContextOutput {
self,
mut stream: impl Stream<Item = String> + Send + Unpin,
) -> impl Stream<Item = String> + Send {
// if the first chunk consists of a synchronously-available Suspend,
// inject_meta_context can accidentally run a tick before it, but the Suspend
// when both are available. waiting a tick before awaiting the first chunk
// in the Stream ensures that this always runs after that first chunk
// see https://github.com/leptos-rs/leptos/issues/3976 for the original issue
leptos::task::tick().await;
// wait for the first chunk of the stream, to ensure our components hve run
let mut first_chunk = stream.next().await.unwrap_or_default();
@@ -242,23 +248,22 @@ impl ServerMetaContextOutput {
let head_loc = first_chunk
.find("</head>")
.expect("you are using leptos_meta without a </head> tag");
let marker_loc =
first_chunk.find("<!--HEAD-->").unwrap_or_else(|| {
let marker_loc = first_chunk
.find("<!--HEAD-->")
.map(|pos| pos + "<!--HEAD-->".len())
.unwrap_or_else(|| {
first_chunk.find("</head>").unwrap_or(head_loc)
});
let (before_marker, after_marker) =
first_chunk.split_at_mut(marker_loc);
let (before_head_close, after_head) =
after_marker.split_at_mut(head_loc - marker_loc);
buf.push_str(before_marker);
buf.push_str(&meta_buf);
if let Some(title) = title {
buf.push_str("<title>");
buf.push_str(&title);
buf.push_str("</title>");
}
buf.push_str(before_head_close);
buf.push_str(&meta_buf);
buf.push_str(after_head);
buf.push_str(after_marker);
buf
};

View File

@@ -7,6 +7,9 @@ use leptos::{
/// Injects an [`HTMLLinkElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement) into the document
/// head that loads a stylesheet from the URL given by the `href` property.
///
/// Note that this does *not* work with the `cargo-leptos` `hash-files` feature: if you are using file
/// hashing, you should use [`HashedStylesheet`](crate::HashedStylesheet).
///
/// ```
/// use leptos::prelude::*;
/// use leptos_meta::*;

View File

@@ -9,8 +9,8 @@ rust-version.workspace = true
edition.workspace = true
[dependencies]
serde = "1.0"
thiserror = { workspace = true }
serde = { workspace = true, default-features = true }
thiserror = { workspace = true , default-features = true }
[dev-dependencies]
serde_json = { workspace = true }
serde_json = { workspace = true , default-features = true }

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

@@ -19,9 +19,9 @@ leptos_router = { path = "../../router" }
log = "0.4.0"
simple_logger = "5.0"
serde = { version = "1.0", features = ["derive"] }
axum = { version = "0.7.0", optional = true, features = ["macros"] }
tower = { version = "0.4.0", optional = true }
tower-http = { version = "0.5.0", features = ["fs"], optional = true }
axum = { version = "0.8.0", optional = true, features = ["macros"] }
tower = { version = "0.5.0", optional = true }
tower-http = { version = "0.6.0", features = ["fs"], optional = true }
tokio = { version = "1.0", features = ["full"], optional = true }
http = { version = "1.0" }
sqlx = { version = "0.8.0", features = [
@@ -30,10 +30,13 @@ sqlx = { version = "0.8.0", features = [
], optional = true }
thiserror = "1.0"
wasm-bindgen = "0.2.0"
axum_session_auth = { version = "0.14.0", features = [], optional = true }
axum_session = { version = "0.14.0", features = [], optional = true }
axum_session_sqlx = { version = "0.3.0", features = [ "sqlite", "tls-rustls"], optional = true }
bcrypt = { version = "0.15.0", optional = true }
axum_session_auth = { version = "0.16.0", features = [], optional = true }
axum_session = { version = "0.16.0", features = [], optional = true }
axum_session_sqlx = { version = "0.5.0", features = [
"sqlite",
"tls-rustls",
], optional = true }
bcrypt = { version = "0.17.0", optional = true }
async-trait = { version = "0.1.0", optional = true }
[features]

View File

@@ -185,7 +185,7 @@ pub async fn foo() -> Result<String, ServerFnError> {
pub async fn get_user() -> Result<Option<User>, ServerFnError> {
use crate::todo::ssr::auth;
let auth = auth()?;
let auth = auth().await?;
Ok(auth.current_user)
}
@@ -199,7 +199,7 @@ pub async fn login(
use self::ssr::*;
let pool = pool()?;
let auth = auth()?;
let auth = auth().await?;
let (user, UserPasshash(expected_passhash)) =
User::get_from_username_with_passhash(username, &pool)
@@ -229,7 +229,7 @@ pub async fn signup(
use self::ssr::*;
let pool = pool()?;
let auth = auth()?;
let auth = auth().await?;
if password != password_confirmation {
return Err(ServerFnError::ServerError(
@@ -264,7 +264,7 @@ pub async fn signup(
pub async fn logout() -> Result<(), ServerFnError> {
use self::ssr::*;
let auth = auth()?;
let auth = auth().await?;
auth.logout_user();
leptos_axum::redirect("/");

View File

@@ -1,62 +1,12 @@
use axum::{
body::Body as AxumBody,
extract::{Path, State},
http::Request,
response::{IntoResponse, Response},
routing::get,
Router,
};
use axum::Router;
use axum_session::{SessionConfig, SessionLayer, SessionStore};
use axum_session_auth::{AuthConfig, AuthSessionLayer};
use axum_session_sqlx::SessionSqlitePool;
use leptos::{
config::get_configuration, logging::log, prelude::provide_context,
};
use leptos_axum::{
generate_route_list, handle_server_fns_with_context, LeptosRoutes,
};
use session_auth_axum::{
auth::{ssr::AuthSession, User},
state::AppState,
todo::*,
};
use leptos::{config::get_configuration, logging::log};
use leptos_axum::{generate_route_list, LeptosRoutes};
use session_auth_axum::{auth::User, state::AppState, todo::*};
use sqlx::{sqlite::SqlitePoolOptions, SqlitePool};
async fn server_fn_handler(
State(app_state): State<AppState>,
auth_session: AuthSession,
path: Path<String>,
request: Request<AxumBody>,
) -> impl IntoResponse {
log!("{:?}", path);
handle_server_fns_with_context(
move || {
provide_context(auth_session.clone());
provide_context(app_state.pool.clone());
},
request,
)
.await
}
async fn leptos_routes_handler(
auth_session: AuthSession,
state: State<AppState>,
req: Request<AxumBody>,
) -> Response {
let State(app_state) = state.clone();
let handler = leptos_axum::render_route_with_context(
app_state.routes.clone(),
move || {
provide_context(auth_session.clone());
provide_context(app_state.pool.clone());
},
move || shell(app_state.leptos_options.clone()),
);
handler(state, req).await.into_response()
}
#[tokio::main]
async fn main() {
simple_logger::init_with_level(log::Level::Info)
@@ -82,18 +32,6 @@ async fn main() {
eprintln!("{e:?}");
}
// Explicit server function registration is no longer required
// on the main branch. On 0.3.0 and earlier, uncomment the lines
// below to register the server functions.
// _ = GetTodos::register();
// _ = AddTodo::register();
// _ = DeleteTodo::register();
// _ = Login::register();
// _ = Logout::register();
// _ = Signup::register();
// _ = GetUser::register();
// _ = Foo::register();
// Setting this to None means we'll be using cargo-leptos and its env vars
let conf = get_configuration(None).unwrap();
let leptos_options = conf.leptos_options;
@@ -108,11 +46,10 @@ async fn main() {
// build our application with a route
let app = Router::new()
.route(
"/api/*fn_name",
get(server_fn_handler).post(server_fn_handler),
)
.leptos_routes_with_handler(routes, get(leptos_routes_handler))
.leptos_routes(&app_state, routes, {
let options = app_state.leptos_options.clone();
move || shell(options.clone())
})
.fallback(leptos_axum::file_and_error_handler::<AppState, _>(shell))
.layer(
AuthSessionLayer::<User, i64, SessionSqlitePool, SqlitePool>::new(

View File

@@ -1,7 +1,7 @@
use crate::{auth::*, error_template::ErrorTemplate};
use leptos::prelude::*;
use leptos_meta::*;
use leptos_router::{components::*, *};
use leptos_router::{components::*, path};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
@@ -16,19 +16,21 @@ pub struct Todo {
#[cfg(feature = "ssr")]
pub mod ssr {
use super::Todo;
use crate::auth::{ssr::AuthSession, User};
use crate::{
auth::{ssr::AuthSession, User},
state::AppState,
};
use leptos::prelude::*;
use sqlx::SqlitePool;
pub fn pool() -> Result<SqlitePool, ServerFnError> {
use_context::<SqlitePool>()
with_context::<AppState, _>(|state| state.pool.clone())
.ok_or_else(|| ServerFnError::ServerError("Pool missing.".into()))
}
pub fn auth() -> Result<AuthSession, ServerFnError> {
use_context::<AuthSession>().ok_or_else(|| {
ServerFnError::ServerError("Auth session missing.".into())
})
pub async fn auth() -> Result<AuthSession, ServerFnError> {
let auth = leptos_axum::extract().await?;
Ok(auth)
}
#[derive(sqlx::FromRow, Clone)]
@@ -165,7 +167,7 @@ pub fn TodoApp() -> impl IntoView {
", "
<A href="/login">"Login"</A>
", "
<span>{format!("Login error: {}", e)}</span>
<span>{format!("Login error: {e}")}</span>
}
.into_any()
}

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.0-rc3"
version = "0.2.2"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
@@ -12,28 +12,28 @@ edition.workspace = true
[dependencies]
any_spawner = { workspace = true }
or_poisoned = { workspace = true }
futures = "0.3.31"
futures = { workspace = true, default-features = true }
hydration_context = { workspace = true, optional = true }
pin-project-lite = "0.2.15"
rustc-hash = "2.0"
serde = { version = "1.0", features = ["derive"], optional = true }
slotmap = "1.0"
thiserror = { workspace = true }
tracing = { version = "0.1.41", optional = true }
guardian = "1.2"
async-lock = "3.4.0"
send_wrapper = { version = "0.6.0", features = ["futures"] }
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 }
slotmap = { 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 = { 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.72", features = ["console"] }
web-sys = { version = "0.3.77", features = ["console"] }
[dev-dependencies]
tokio = { version = "1.43", features = ["rt-multi-thread", "macros"] }
tokio-test = { version = "0.4.4" }
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 = "0.4.1"
rustc_version = { workspace = true, default-features = true }
[features]
nightly = []

View File

@@ -234,7 +234,8 @@ macro_rules! spawn_derived {
subscribers: SubscriberSet::new(),
state: AsyncDerivedState::Clean,
version: 0,
suspenses: Vec::new()
suspenses: Vec::new(),
pending_suspenses: Vec::new()
}));
let value = Arc::new(AsyncRwLock::new($initial));
let wakers = Arc::new(RwLock::new(Vec::new()));
@@ -364,7 +365,7 @@ macro_rules! spawn_derived {
// generate and assign new value
loading.store(true, Ordering::Relaxed);
let (this_version, suspense_ids) = {
let this_version = {
let mut guard = inner.write().or_poisoned();
guard.version += 1;
let version = guard.version;
@@ -372,14 +373,17 @@ macro_rules! spawn_derived {
.into_iter()
.map(|sc| sc.task_id())
.collect::<Vec<_>>();
(version, suspense_ids)
guard.pending_suspenses.extend(suspense_ids);
version
};
let new_value = fut.await;
drop(suspense_ids);
let latest_version = inner.read().or_poisoned().version;
let latest_version = {
let mut guard = inner.write().or_poisoned();
drop(mem::take(&mut guard.pending_suspenses));
guard.version
};
if latest_version == this_version {
Self::set_inner_value(new_value, value, wakers, inner, loading, Some(ready_tx)).await;
@@ -480,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!(
@@ -514,7 +521,12 @@ impl<T: 'static> ArcAsyncDerived<T> {
{
let fun = move || {
let fut = fun();
async move { SendOption::new(Some(fut.await)) }
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!(
@@ -556,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!(
@@ -592,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,
@@ -641,6 +659,14 @@ impl<T: 'static> Write for ArcAsyncDerived<T> {
type Value = Option<T>;
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
// increment the version, such that a rerun triggered previously does not overwrite this
// new value
let mut guard = self.inner.write().or_poisoned();
guard.version += 1;
// tell any suspenses to stop waiting for this
drop(mem::take(&mut guard.pending_suspenses));
Some(MappedMut::new(
WriteGuard::new(self.clone(), self.value.blocking_write()),
|v| v.deref(),
@@ -651,6 +677,14 @@ impl<T: 'static> Write for ArcAsyncDerived<T> {
fn try_write_untracked(
&self,
) -> Option<impl DerefMut<Target = Self::Value>> {
// increment the version, such that a rerun triggered previously does not overwrite this
// new value
let mut guard = self.inner.write().or_poisoned();
guard.version += 1;
// tell any suspenses to stop waiting for this
drop(mem::take(&mut guard.pending_suspenses));
Some(MappedMut::new(
self.value.blocking_write(),
|v| v.deref(),

View File

@@ -14,8 +14,10 @@ use crate::{
unwrap_signal,
};
use core::fmt::Debug;
use or_poisoned::OrPoisoned;
use std::{
future::Future,
mem,
ops::{Deref, DerefMut},
panic::Location,
};
@@ -27,7 +29,7 @@ use std::{
/// values that depend on it that it has changed.
///
/// This is an arena-allocated type, which is `Copy` and is disposed when its reactive
/// [`Owner`](crate::owner::Owner) cleans up. For a reference-counted signal that livesas
/// [`Owner`](crate::owner::Owner) cleans up. For a reference-counted signal that lives as
/// as long as a reference to it is alive, see [`ArcAsyncDerived`].
///
/// ## Examples
@@ -349,6 +351,17 @@ where
let guard = self
.inner
.try_with_value(|n| n.value.blocking_write_arc())?;
self.inner.try_with_value(|n| {
let mut guard = n.inner.write().or_poisoned();
// increment the version, such that a rerun triggered previously does not overwrite this
// new value
guard.version += 1;
// tell any suspenses to stop waiting for this
drop(mem::take(&mut guard.pending_suspenses));
});
Some(MappedMut::new(
WriteGuard::new(*self, guard),
|v| v.deref(),
@@ -359,6 +372,16 @@ where
fn try_write_untracked(
&self,
) -> Option<impl DerefMut<Target = Self::Value>> {
self.inner.try_with_value(|n| {
let mut guard = n.inner.write().or_poisoned();
// increment the version, such that a rerun triggered previously does not overwrite this
// new value
guard.version += 1;
// tell any suspenses to stop waiting for this
drop(mem::take(&mut guard.pending_suspenses));
});
self.inner
.try_with_value(|n| n.value.blocking_write_arc())
.map(|inner| {

View File

@@ -1,3 +1,4 @@
use super::suspense::TaskHandle;
use crate::{
channel::Sender,
computed::suspense::SuspenseContext,
@@ -22,6 +23,7 @@ pub(crate) struct ArcAsyncDerivedInner {
pub state: AsyncDerivedState,
pub version: usize,
pub suspenses: Vec<SuspenseContext>,
pub pending_suspenses: Vec<TaskHandle>,
}
#[derive(Debug, PartialEq, Eq)]

View File

@@ -42,6 +42,18 @@ impl<Fut> ScopedFuture<Fut> {
fut,
}
}
/// Wraps the given `Future` by taking the current [`Owner`] re-setting it as the
/// active owner every time the inner `Future` is polled. Always untracks, i.e., clears
/// the active [`Observer`] when polled.
pub fn new_untracked(fut: Fut) -> Self {
let owner = Owner::current().unwrap_or_default();
Self {
owner,
observer: None,
fut,
}
}
}
impl<Fut: Future> Future for ScopedFuture<Fut> {

View File

@@ -15,7 +15,7 @@ pub struct MemoInner<T, S>
where
S: Storage<T>,
{
/// Must always be aquired *after* the reactivity lock
/// Must always be acquired *after* the reactivity lock
pub(crate) value: Arc<RwLock<Option<S::Wrapped>>>,
#[allow(clippy::type_complexity)]
pub(crate) fun: Arc<dyn Fn(Option<T>) -> (T, bool) + Send + Sync>,
@@ -137,7 +137,7 @@ where
})
});
// Two locks are aquired, so order matters.
// Two locks are acquired, so order matters.
let reactivity_lock = self.reactivity.write().or_poisoned();
{
// Safety: Can block endlessly if the user is has a ReadGuard on the value

View File

@@ -27,7 +27,7 @@ use std::{fmt::Debug, hash::Hash, panic::Location};
/// not re-run the calculation when a source signal changes until they are read again.
///
/// This is an arena-allocated type, which is `Copy` and is disposed when its reactive
/// [`Owner`](crate::owner::Owner) cleans up. For a reference-counted signal that livesas
/// [`Owner`](crate::owner::Owner) cleans up. For a reference-counted signal that lives as
/// as long as a reference to it is alive, see [`ArcMemo`].
///
/// ```

View File

@@ -14,7 +14,7 @@ pub struct SpecialNonReactiveZone;
/// Exits the "special non-reactive zone" when dropped.
#[derive(Debug)]
pub struct SpecialNonReactiveZoneGuard;
pub struct SpecialNonReactiveZoneGuard(bool);
use pin_project_lite::pin_project;
use std::{
@@ -31,8 +31,8 @@ thread_local! {
impl SpecialNonReactiveZone {
/// Suppresses warnings about non-reactive accesses until the guard is dropped.
pub fn enter() -> SpecialNonReactiveZoneGuard {
IS_SPECIAL_ZONE.set(true);
SpecialNonReactiveZoneGuard
let prev = IS_SPECIAL_ZONE.replace(true);
SpecialNonReactiveZoneGuard(prev)
}
#[cfg(all(debug_assertions, feature = "effects"))]
@@ -48,7 +48,7 @@ impl SpecialNonReactiveZone {
impl Drop for SpecialNonReactiveZoneGuard {
fn drop(&mut self) {
IS_SPECIAL_ZONE.set(false);
IS_SPECIAL_ZONE.set(self.0);
}
}

View File

@@ -104,15 +104,7 @@ pub mod prelude {
#[allow(unused)]
#[doc(hidden)]
pub fn log_warning(text: Arguments) {
#[cfg(feature = "tracing")]
{
tracing::warn!(text);
}
#[cfg(all(
not(feature = "tracing"),
target_arch = "wasm32",
target_os = "unknown"
))]
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
{
web_sys::console::warn_1(&text.to_string().into());
}

View File

@@ -1,4 +1,4 @@
//! The reactive ownership model, which manages effect cancelation, cleanups, and arena allocation.
//! The reactive ownership model, which manages effect cancellation, cleanups, and arena allocation.
#[cfg(feature = "hydration")]
use hydration_context::SharedContext;
@@ -32,7 +32,7 @@ pub use storage::*;
pub use stored_value::{store_value, FromLocal, StoredValue};
/// A reactive owner, which manages
/// 1) the cancelation of [`Effect`](crate::effect::Effect)s,
/// 1) the cancellation of [`Effect`](crate::effect::Effect)s,
/// 2) providing and accessing environment data via [`provide_context`] and [`use_context`],
/// 3) running cleanup functions defined via [`Owner::on_cleanup`], and
/// 4) an arena storage system to provide `Copy` handles via [`ArenaItem`], which is what allows
@@ -167,6 +167,7 @@ impl Owner {
.map(|parent| parent.read().or_poisoned().arena.clone())
.unwrap_or_default(),
paused: false,
joined_owners: Vec::new(),
})),
#[cfg(feature = "hydration")]
shared_context,
@@ -201,6 +202,7 @@ impl Owner {
#[cfg(feature = "sandboxed-arenas")]
arena: Default::default(),
paused: false,
joined_owners: Vec::new(),
})),
#[cfg(feature = "hydration")]
shared_context,
@@ -226,6 +228,7 @@ impl Owner {
#[cfg(feature = "sandboxed-arenas")]
arena,
paused,
joined_owners: Vec::new(),
})),
#[cfg(feature = "hydration")]
shared_context: self.shared_context.clone(),
@@ -288,12 +291,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));
}
}
@@ -455,6 +464,7 @@ pub(crate) struct OwnerInner {
#[cfg(feature = "sandboxed-arenas")]
arena: Arc<RwLock<ArenaMap>>,
paused: bool,
joined_owners: Vec<WeakOwner>,
}
impl Debug for OwnerInner {

View File

@@ -6,6 +6,15 @@ use std::{
};
impl Owner {
#[doc(hidden)]
pub fn join_contexts(&self, other: &Owner) {
self.inner
.write()
.or_poisoned()
.joined_owners
.push(other.downgrade());
}
fn provide_context<T: Send + Sync + 'static>(&self, value: T) {
self.inner
.write()
@@ -21,22 +30,31 @@ impl Owner {
fn take_context<T: 'static>(&self) -> Option<T> {
let ty = TypeId::of::<T>();
let mut inner = self.inner.write().or_poisoned();
let mut parent = inner.parent.as_ref().and_then(|p| p.upgrade());
let contexts = &mut inner.contexts;
if let Some(context) = contexts.remove(&ty) {
context.downcast::<T>().ok().map(|n| *n)
} else {
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 parent = inner.parent.as_ref().and_then(|p| p.upgrade());
let joined = inner
.joined_owners
.iter()
.flat_map(|owner| owner.upgrade().map(|owner| 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());
}
}
}
None
@@ -49,22 +67,31 @@ impl Owner {
) -> Option<R> {
let ty = TypeId::of::<T>();
let inner = self.inner.read().or_poisoned();
let mut parent = inner.parent.as_ref().and_then(|p| p.upgrade());
let contexts = &inner.contexts;
let reference = if let Some(context) = contexts.get(&ty) {
context.downcast_ref::<T>()
} else {
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 parent = inner.parent.as_ref().and_then(|p| p.upgrade());
let joined = inner
.joined_owners
.iter()
.flat_map(|owner| owner.upgrade().map(|owner| 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());
}
}
}
None
@@ -78,22 +105,31 @@ impl Owner {
) -> Option<R> {
let ty = TypeId::of::<T>();
let mut inner = self.inner.write().or_poisoned();
let mut parent = inner.parent.as_ref().and_then(|p| p.upgrade());
let contexts = &mut inner.contexts;
let reference = if let Some(context) = contexts.get_mut(&ty) {
context.downcast_mut::<T>()
} else {
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 parent = inner.parent.as_ref().and_then(|p| p.upgrade());
let joined = inner
.joined_owners
.iter()
.flat_map(|owner| owner.upgrade().map(|owner| 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());
}
}
}
None
@@ -236,11 +272,11 @@ pub fn provide_context<T: Send + Sync + 'static>(value: T) {
///
/// Effect::new(move |_| {
/// // each use_context clones the value
/// let value =
/// use_context::<String>().expect("could not find i32 in context");
/// let value = use_context::<String>()
/// .expect("could not find String in context");
/// assert_eq!(value, "foo");
/// let value2 =
/// use_context::<String>().expect("could not find i32 in context");
/// let value2 = use_context::<String>()
/// .expect("could not find String in context");
/// assert_eq!(value2, "foo");
/// });
/// });
@@ -284,11 +320,11 @@ pub fn use_context<T: Clone + 'static>() -> Option<T> {
///
/// Effect::new(move |_| {
/// // each use_context clones the value
/// let value =
/// use_context::<String>().expect("could not find i32 in context");
/// let value = use_context::<String>()
/// .expect("could not find String in context");
/// assert_eq!(value, "foo");
/// let value2 =
/// use_context::<String>().expect("could not find i32 in context");
/// let value2 = use_context::<String>()
/// .expect("could not find String in context");
/// assert_eq!(value2, "foo");
/// });
/// });

View File

@@ -9,7 +9,7 @@ use std::{
/// An optional value that can always be sent between threads, even if its inner value
/// in the `Some(_)` case would not be threadsafe.
///
/// This struct can be derefenced to `Option<T>`.
/// This struct can be dereferenced to `Option<T>`.
///
/// If it has been given a local (`!Send`) value, that value is wrapped in a [`SendWrapper`], which
/// allows sending it between threads but will panic if it is accessed or updated from a

View File

@@ -22,11 +22,11 @@ use std::{
/// allowing you to read or write directly to one of its field.
///
/// Tracking the mapped signal tracks changes to *any* part of the signal, and updating the signal notifies
/// and notifies *all* depenendencies of the signal. This is not a mechanism for fine-grained reactive updates
/// and notifies *all* dependencies of the signal. This is not a mechanism for fine-grained reactive updates
/// to more complex data structures. Instead, it allows you to provide a signal-like API for wrapped types
/// without exposing the original type directly to users.
pub struct ArcMappedSignal<T> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
#[allow(clippy::type_complexity)]
try_read_untracked: Arc<
@@ -44,7 +44,7 @@ pub struct ArcMappedSignal<T> {
impl<T> Clone for ArcMappedSignal<T> {
fn clone(&self) -> Self {
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: self.defined_at,
try_read_untracked: self.try_read_untracked.clone(),
try_write: self.try_write.clone(),
@@ -67,7 +67,7 @@ impl<T> ArcMappedSignal<T> {
U: Send + Sync + 'static,
{
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
try_read_untracked: {
let this = inner.clone();
@@ -110,7 +110,7 @@ impl<T> ArcMappedSignal<T> {
impl<T> Debug for ArcMappedSignal<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut partial = f.debug_struct("ArcMappedSignal");
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
partial.field("defined_at", &self.defined_at);
partial.finish()
}
@@ -118,11 +118,11 @@ impl<T> Debug for ArcMappedSignal<T> {
impl<T> DefinedAt for ArcMappedSignal<T> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
@@ -224,11 +224,11 @@ where
/// allowing you to read or write directly to one of its field.
///
/// Tracking the mapped signal tracks changes to *any* part of the signal, and updating the signal notifies
/// and notifies *all* depenendencies of the signal. This is not a mechanism for fine-grained reactive updates
/// and notifies *all* dependencies of the signal. This is not a mechanism for fine-grained reactive updates
/// to more complex data structures. Instead, it allows you to provide a signal-like API for wrapped types
/// without exposing the original type directly to users.
pub struct MappedSignal<T, S = SyncStorage> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
inner: StoredValue<ArcMappedSignal<T>, S>,
}
@@ -246,7 +246,7 @@ impl<T> MappedSignal<T> {
U: Send + Sync + 'static,
{
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: {
let this = ArcRwSignal::from(inner);
@@ -269,7 +269,7 @@ impl<T> Clone for MappedSignal<T> {
impl<T> Debug for MappedSignal<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut partial = f.debug_struct("MappedSignal");
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
partial.field("defined_at", &self.defined_at);
partial.finish()
}
@@ -277,11 +277,11 @@ impl<T> Debug for MappedSignal<T> {
impl<T> DefinedAt for MappedSignal<T> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
@@ -352,7 +352,7 @@ where
#[track_caller]
fn from(value: ArcMappedSignal<T>) -> Self {
MappedSignal {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: StoredValue::new(value),
}

View File

@@ -29,7 +29,7 @@ use std::{hash::Hash, ops::DerefMut, panic::Location, sync::Arc};
/// > Each of these has a related `_untracked()` method, which updates the signal
/// > without notifying subscribers. Untracked updates are not desirable in most
/// > cases, as they cause “tearing” between the signals value and its observed
/// > value. If you want a non-reactive container, used [`ArenaItem`] instead.
/// > value. If you want a non-reactive container, use [`ArenaItem`] instead.
///
/// ## Examples
/// ```
@@ -118,7 +118,7 @@ where
#[track_caller]
fn from(value: ArcWriteSignal<T>) -> Self {
WriteSignal {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: ArenaItem::new_with_storage(value),
}
@@ -132,7 +132,7 @@ where
#[track_caller]
fn from_local(value: ArcWriteSignal<T>) -> Self {
WriteSignal {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: ArenaItem::new_with_storage(value),
}

View File

@@ -643,7 +643,7 @@ pub trait IntoInner {
/// The type of the value contained in the signal.
type Value;
/// Returns the inner value if this is the only reference to to the signal.
/// Returns the inner value if this is the only reference to the signal.
/// Otherwise, returns `None` and drops this reference.
/// # Panics
/// Panics if the inner lock is poisoned.

View File

@@ -11,7 +11,8 @@ pub mod read {
},
signal::{
guards::{Mapped, Plain, ReadGuard},
ArcReadSignal, ArcRwSignal, ReadSignal, RwSignal,
ArcMappedSignal, ArcReadSignal, ArcRwSignal, MappedSignal,
ReadSignal, RwSignal,
},
traits::{
DefinedAt, Dispose, Get, Read, ReadUntracked, ReadValue, Track,
@@ -807,6 +808,16 @@ pub mod read {
}
}
impl<T> From<MappedSignal<T>> for Signal<T>
where
T: Clone + Send + Sync + 'static,
{
#[track_caller]
fn from(value: MappedSignal<T>) -> Self {
Self::derive(move || value.get())
}
}
impl<T> From<RwSignal<T, LocalStorage>> for Signal<T, LocalStorage>
where
T: 'static,
@@ -839,6 +850,16 @@ pub mod read {
}
}
impl<T> From<ArcMappedSignal<T>> for Signal<T>
where
T: Clone + Send + Sync + 'static,
{
#[track_caller]
fn from(value: ArcMappedSignal<T>) -> Self {
MappedSignal::from(value).into()
}
}
impl<T> From<ArcRwSignal<T>> for Signal<T, LocalStorage>
where
T: Send + Sync + 'static,

View File

@@ -1,6 +1,6 @@
[package]
name = "reactive_stores"
version = "0.2.0-rc3"
version = "0.2.2"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
@@ -10,19 +10,19 @@ rust-version.workspace = true
edition.workspace = true
[dependencies]
guardian = "1.2"
itertools = { workspace = true }
guardian = { workspace = true, default-features = true }
itertools = { workspace = true , default-features = true }
or_poisoned = { workspace = true }
paste = "1.0"
paste = { workspace = true, default-features = true }
reactive_graph = { workspace = true }
rustc-hash = "2.0"
rustc-hash = { workspace = true, default-features = true }
reactive_stores_macro = { workspace = true }
dashmap = "6.1"
send_wrapper = "0.6.0"
dashmap = { workspace = true, default-features = true }
send_wrapper = { workspace = true, default-features = true }
[dev-dependencies]
tokio = { version = "1.43", features = ["rt-multi-thread", "macros"] }
tokio-test = { version = "0.4.4" }
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"] }
leptos = { path = "../leptos", features = ["csr"] }

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