Compare commits

...

190 Commits

Author SHA1 Message Date
Greg Johnston
2faae43d5f v0.8.0-beta 2025-03-19 21:02:10 -04:00
Nicolas Cura
6760c87e83 Merge pull request #3727 from NCura/patch-1
Fix typo
2025-03-19 21:02:10 -04:00
autofix-ci[bot]
e19e42c650 [autofix.ci] apply automated fixes 2025-03-19 23:42:02 +00:00
zakstucke
fb4be49ebf feat: remove SendWrapper from the external interface of LocalResource (#3715) 2025-03-19 19:26:14 -04:00
Greg Johnston
0b4cbbc17d Merge remote-tracking branch 'origin' into leptos_0.8 2025-03-16 14:27:23 -04:00
Greg Johnston
dbbeb7c6ef fix: don't retrigger rendering when only param has changed (closes #3719( 2025-03-16 14:26:23 -04:00
Greg Johnston
36aef2565d chore: merge issues 2025-03-16 14:25:31 -04:00
Greg Johnston
a2a7eb8a2a Merge remote-tracking branch 'origin' into leptos_0.8 2025-03-16 14:21:21 -04:00
Zak Stucke
97e22e2506 Extra trait impls for MaybeSendWrapper 2025-03-16 14:20:35 -04:00
autofix-ci[bot]
8bedacb0c7 [autofix.ci] apply automated fixes 2025-03-16 14:20:35 -04:00
Zak Stucke
56b7b9a16a Remove SendWrapper from the external interface of LocalResource, by internalising a MaybeSendWrapper inside ArcAsyncDerived 2025-03-16 14:20:35 -04:00
Greg Johnston
d04d4c77f9 Merge pull request #3720 from metatoaster/regression_tests_3502
test: regression from #3502
2025-03-16 14:16:05 -04:00
Tommy Yu
5c75928b5b test: avoiding testdriver browser contention
- The number of tests have increased sufficiently that the browser test
  instances spawned by the driver are choked out on CI.
2025-03-16 22:43:21 +13:00
Tommy Yu
abc5631654 test: regression from #3502
- Also as reported in #3719, which has an actual minimum example.
- The "quicker" test had a reset but that runs into timing issue with
  the way this reset is done too soon after resource usage, so leaving
  this out and we will just trust the bigger counters.
2025-03-16 22:43:21 +13:00
Tommy Yu
40e5288ac1 fix instrumented use_context
- It shouldn't be in on_cleanup, move into it from the component to
  avoid BorrowMut error.
2025-03-16 22:43:21 +13:00
Greg Johnston
335934d40e Merge pull request #3716 from zakstucke/string-opt
view!{} macro optimisation: don't wrap string types in closures when passing to ToChildren
2025-03-15 10:56:40 -04:00
Greg Johnston
6ee72f42e2 Merge pull request #3687 from leptos-rs/3671
Various issues related to setting signals and context in cleanups
2025-03-15 10:34:53 -04:00
Thomas Versteeg
93af23a970 feat: allow closures for shell parameter in file_and_error_handler* (#3711) 2025-03-15 10:24:09 -04:00
Danik Vitek
95e8ae84af feat(reactive_stores): Replace AsRef bound of StoreFieldIterator blanket impl with Len bound (#3701) 2025-03-15 10:23:09 -04:00
Danik Vitek
5cfe7f6b5e fix: make tuple struct field locator in Patch impl Index instead of usize (#3700) 2025-03-15 10:19:41 -04:00
Greg Johnston
0404efd5c3 fix: ensure that store subfield mutations notify from the root down (closes #3704) (#3714) 2025-03-15 10:04:52 -04:00
Zak Stucke
93173c1400 view!} macro optimisation: don't wrap string types in closures when passing to ToChildren 2025-03-15 03:17:20 +00:00
Tommy Yu
cd2904f6a6 chore: prep common base to share example with 0.8 2025-03-15 14:35:55 +13:00
Greg Johnston
6b453845f9 fix: allow NodeRef to work with AttributeInterceptor (closes #3697) 2025-03-13 21:38:28 -04:00
zakstucke
111b84ce3b feat: allow LocalResource sync methods to be used outside Suspense (#3708) 2025-03-13 21:36:51 -04:00
zakstucke
5633148047 feat: allow LocalResource sync methods to be used outside Suspense (#3708) 2025-03-13 09:28:00 -04:00
autofix-ci[bot]
4edb012de3 [autofix.ci] apply automated fixes 2025-03-13 01:16:55 +00:00
Greg Johnston
b2bea2e6b7 fix: support for websockets under default features 2025-03-12 21:06:52 -04:00
Greg Johnston
acbd6378a8 chore: bump version for 0.8 2025-03-12 20:29:46 -04:00
Greg Johnston
b7462aab10 fix: broken type inference for Action::new_unsync (closes #3328) (#3705) 2025-03-12 19:12:46 -04:00
Greg Johnston
7593540774 Merge remote-tracking branch 'origin' into leptos_0.8 2025-03-12 16:16:10 -04:00
Greg Johnston
ed915f8e06 Merge pull request #3656 from ealmloff/websockets
Add websocket support for server functions
2025-03-12 16:13:41 -04:00
Greg Johnston
f65d87d566 chore: allow dead code in example 2025-03-10 21:07:41 -04:00
Greg Johnston
5034539411 chore: update server_fns_axum example 2025-03-10 21:06:36 -04:00
Greg Johnston
bc48aa4228 chore: reexport Bytes to make it easier to implement Client 2025-03-10 21:06:26 -04:00
Greg Johnston
d2c81fe955 change: enable url and json by default on server_fn 2025-03-10 16:02:50 -04:00
Greg Johnston
28eb96831a chore: fix up doctests on server_fn crate 2025-03-10 15:57:59 -04:00
Greg Johnston
330920eae2 chore: clippy 2025-03-10 10:14:46 -04:00
Greg Johnston
a94bc0a6da fix: only store a weak reference to an Owner in the current thread (see #3671) 2025-03-10 10:14:46 -04:00
Greg Johnston
f85e01f4d6 fix: do not panic unnecessarily in try_ methods on Arena (closes #3671) 2025-03-10 10:14:46 -04:00
autofix-ci[bot]
599c87c88a [autofix.ci] apply automated fixes 2025-03-10 14:03:07 +00:00
Evan Almloff
3ca98279e1 forward server fn visibility 2025-03-10 08:52:32 -05:00
mahdi739
a730bffe13 fix: Ensure reactive functions passed to TextProp are kept reactive (closes: #3689) (#3690) 2025-03-09 19:40:28 -04:00
Greg Johnston
d0bf843821 Merge pull request #3692 from QuartzLibrary/immediate-effect
`ImmediateEffect` follow up
2025-03-09 13:50:26 -04:00
QuartzLibrary
7250bc312e docs: clarify need for ThreadId 2025-03-09 11:49:23 +00:00
QuartzLibrary
0e8242f94c feat: simplify ImmediateEffect 2025-03-08 18:40:52 +00:00
QuartzLibrary
439b41f0e8 test: parallel ImmediateEffect r/w access 2025-03-08 18:35:16 +00:00
Evan Almloff
18570e970c Fix clippy 2025-03-07 17:19:24 -06:00
autofix-ci[bot]
787bf385d3 [autofix.ci] apply automated fixes 2025-03-07 23:18:19 +00:00
Evan Almloff
b6d2808671 Document the protocol trait and implementations 2025-03-07 17:12:49 -06:00
Evan Almloff
2e4d94b6c6 Add encoding suffix to the encoding types and revert renaming the post encodings 2025-03-07 17:12:49 -06:00
autofix-ci[bot]
66f9c8c999 [autofix.ci] apply automated fixes 2025-03-07 21:26:39 +00:00
Greg Johnston
352080d91a fix: don't use ws feature of Axum on JS-fetch/no-default platform 2025-03-07 16:19:11 -05:00
Greg Johnston
1e579614a5 chore: remove unused import 2025-03-07 15:55:28 -05:00
Greg Johnston
a4e47d4086 Merge pull request #3650 from QuartzLibrary/immediate-effect
`ImmediateEffect`
2025-03-07 15:18:50 -05:00
Greg Johnston
3164721fdb fix: untrack in NodeRef::on_load() to avoid re-triggering it if you read something reactively (closes #3684) (#3686) 2025-03-07 12:07:46 -05:00
Evan Almloff
fee4bccb32 Create post type aliases for encodings 2025-03-07 08:40:08 -06:00
mahdi739
4ba9f67440 feat: implement IntoClass for store fields (#3670)
* Implement `IntoClass` for store fields

* [autofix.ci] apply automated fixes

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

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

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-03-07 08:22:59 -05:00
Saber Haj Rabiee
2242ad1847 fix: semver and feature handy script for update nightly (#3674) 2025-03-07 08:21:21 -05:00
Greg Johnston
131b18bddb Merge pull request #3677 from sabify/fix-enum-stack
fix: enum stack size
2025-03-07 08:20:20 -05:00
Greg Johnston
5e9d6e2dfd fix: point bind:group to correct location (closes #3678) (#3680) 2025-03-07 08:04:53 -05:00
Saber Haj Rabiee
d76e5bb4ea fix: moved value 2025-03-05 21:21:29 -08:00
Saber Haj Rabiee
f752e32ae3 fix: clippy on large enum variant (https://rust-lang.github.io/rust-clippy/master/index.html#large_enum_variant) 2025-03-05 21:10:30 -08:00
Saber Haj Rabiee
a9197102a6 fix: clippy warning on match usage 2025-03-05 21:08:10 -08:00
Saber Haj Rabiee
58f1bf95e1 fix: clippy clone_on_copy (https://rust-lang.github.io/rust-clippy/master/index.html#clone_on_copy) 2025-03-05 21:07:37 -08:00
autofix-ci[bot]
d84ab6d9bf [autofix.ci] apply automated fixes (attempt 2/3) 2025-03-05 16:23:48 +00:00
autofix-ci[bot]
f069d4478e [autofix.ci] apply automated fixes 2025-03-05 15:20:59 +00:00
Evan Almloff
65b5d55d62 deny missing docs 2025-03-05 09:01:22 -06:00
Evan Almloff
860ad7a221 return an error when the sever doesn't support spawning tasks 2025-03-05 09:00:26 -06:00
Evan Almloff
901e038aa0 update form integration 2025-03-05 08:53:54 -06:00
Evan Almloff
f49f0965bc remove default actix feature 2025-03-05 08:51:31 -06:00
Evan Almloff
bb62d08d3f implement actix integration 2025-03-05 08:47:06 -06:00
Greg Johnston
bfe04593fd Merge pull request #3658 from mahdi739/impl-several-into-traits-for-store-fields-0.8
Implement several into traits for store fields (0.8)
2025-03-04 08:54:02 -05:00
Greg Johnston
38a3aae28e Merge branch 'main' into immediate-effect 2025-03-03 21:02:43 -05:00
martin frances
5eaaff045f chore(clippy): replace mem::replace with Option::replace (#3668) 2025-03-03 18:01:19 -05:00
autofix-ci[bot]
5b484eaec4 [autofix.ci] apply automated fixes (attempt 3/3) 2025-03-03 22:02:22 +00:00
autofix-ci[bot]
e9384e3286 [autofix.ci] apply automated fixes 2025-03-03 21:42:06 +00:00
autofix-ci[bot]
5149ad54db [autofix.ci] apply automated fixes (attempt 2/3) 2025-03-03 21:36:44 +00:00
autofix-ci[bot]
4d9ec54ad1 [autofix.ci] apply automated fixes 2025-03-03 21:22:46 +00:00
Evan Almloff
a1cd7ae9a1 implement reqwest websocket integration 2025-03-03 14:58:37 -06:00
autofix-ci[bot]
083f9c663f [autofix.ci] apply automated fixes 2025-03-03 14:02:22 +00:00
Greg Johnston
63c9549120 fix: ensure cleanups run for all replaced nested routes (closes #3665) (#3666) 2025-03-03 08:19:05 -05:00
Greg Johnston
6232f6482a fix: param segments should not match an empty string that contains only / separator (closes #3527) (#3662) 2025-03-01 12:05:17 -05:00
Greg Johnston
825e89f25c fix: do not double-insert hash character in URLs (closes #3647) (#3661) 2025-03-01 12:05:05 -05:00
Greg Johnston
3dbb251853 fix: tweak bounds on For for backwards-compat (#3663) 2025-03-01 11:50:09 -05:00
zakstucke
98e00fcb3b Erased mode in CI (#3640)
* Erased mode in CI

* Trigger CI

* Rename dev_mode erased_mode plus add to more matrices

* nested routes in separate component fix

* Fix lint

* Small fixes

* Fixes

* proc-macro rustflags cross-compilation workaround with internal erasure feature for leptos_macro

* Re-trigger CI

* fix unrelated doc CI and remove unneeded IntoAttribute trait

* Fix StaticVec rebuild() fn

* Conflict fixes

* Maybe fix

* Bump example toolchain
2025-03-01 07:43:13 -08:00
martin frances
5da4c438d9 chore: examples/server_fns_axum - Bumped various packages (not axum). (#3655)
tower-http
tower
thiserror
strum
notify

axum has breaking changes and so will addressed in a separate PR.
2025-02-28 14:30:03 -05:00
mahdi739
80ed74c075 chore: implement Debug for ArcField and Field (#3660) 2025-02-28 14:29:35 -05:00
Greg Johnston
cdee2a9476 feat: "islands router" for client-side navigations when in islands mode (#3502) 2025-02-28 14:01:33 -05:00
autofix-ci[bot]
c97ab9a72c [autofix.ci] apply automated fixes 2025-02-28 15:27:34 +00:00
Evan Almloff
4fc8972f2b make the into websocket function async 2025-02-28 09:26:37 -06:00
QuartzLibrary
79e9340a9b refactor: improve naming and docs after changes 2025-02-27 23:26:36 +00:00
QuartzLibrary
41d01cedb2 fix: parallel execution in ImmediateEffect 2025-02-27 23:10:08 +00:00
Evan Almloff
b800c009c7 Integrate the websocket protocol with the server function macro 2025-02-27 16:31:06 -06:00
QuartzLibrary
374a020d84 feat: do not store user state in ImmediateEffect 2025-02-27 22:17:06 +00:00
QuartzLibrary
83fcf8663c feat: make ImmediateEffect more robust
The goal here is to make the behavior under recursion better defined.
2025-02-27 22:12:55 +00:00
Greg Johnston
e7a73595de chore: remove unnecessary Debug bound 2025-02-27 17:00:00 -05:00
autofix-ci[bot]
a9a988e0e1 [autofix.ci] apply automated fixes 2025-02-27 14:46:56 +00:00
Mahdi
db10d961df Implement Render, RenderHtml, AttributeValue for store fields 2025-02-27 17:44:07 +03:30
Mahdi
fb608158cb Implement IntoSplitSignal for AtKeyed, AtIndex and DerefedField 2025-02-27 17:16:51 +03:30
Mahdi
1a472ebad1 Implement InnerHtmlValue for store fields 2025-02-27 17:09:34 +03:30
Mahdi
2d1b66a5c6 Implement IntoProperty for store fields 2025-02-27 17:00:33 +03:30
Mahdi
c524b0aefc rename style_store_field macro to style_reactive 2025-02-27 16:55:37 +03:30
Mahdi
e4c977911c Implement IntoStyle and IntoStyleValue for store fields 2025-02-27 16:41:53 +03:30
autofix-ci[bot]
f488d4b5b7 [autofix.ci] apply automated fixes 2025-02-26 19:58:25 +00:00
Evan Almloff
d4cfd0e2cb Move content type to a separate trait 2025-02-26 13:56:59 -06:00
autofix-ci[bot]
a4b0d3408c [autofix.ci] apply automated fixes 2025-02-26 15:44:01 +00:00
Evan Almloff
2037bf12cb Merge branch 'leptos_0.8' into websockets 2025-02-26 09:40:58 -06:00
Evan Almloff
2747a496fc fix formatting 2025-02-26 09:40:22 -06:00
Evan Almloff
c286812116 move the server request and response into a separate trait 2025-02-26 09:36:02 -06:00
Greg Johnston
1e0a9ef189 fix: avoid hydration issues with HashedStylesheet (closes #3633) (#3654) 2025-02-26 09:18:23 -05:00
TERRORW0LF
1363b941bc feat: .map() and .and_then() for OnceResource and LocalResource (#3652) 2025-02-26 09:16:50 -05:00
Evan Almloff
7479010f84 unified http protocol for non-websocket sever functions 2025-02-25 16:20:33 -06:00
QuartzLibrary
f7a1a2cab2 feat: introduce ImmediateEffect 2025-02-25 21:45:28 +00:00
QuartzLibrary
92b82688a6 fix: relax bounds on OrPoisoned blanket impls 2025-02-25 20:59:47 +00:00
Evan Almloff
9b9983af79 expose encoding types 2025-02-25 08:36:04 -06:00
Greg Johnston
04e79a0dc4 chore: clean up fetch example a bit 2025-02-25 08:06:47 -05:00
Greg Johnston
f64951126e chore: merge issues 2025-02-25 07:42:39 -05:00
Greg Johnston
0a29071779 Merge remote-tracking branch 'origin' into leptos_0.8 2025-02-25 07:39:43 -05:00
Greg Johnston
efcb6f6d21 feat: support IntoSplitSignal for (Signal<T>, SignalSetter<T>) (closes #3634) (#3643) 2025-02-25 07:32:37 -05:00
Greg Johnston
42988b1bc1 chore: fix Axum test setup (#3651) 2025-02-25 07:22:12 -05:00
Evan Almloff
6904eec207 remove into/from request/response bounds on server fn 2025-02-24 13:05:37 -06:00
Evan Almloff
7eb8ca702d switch most encodings into the new system 2025-02-24 12:00:02 -06:00
QuartzLibrary
c75397ea75 chore: small refactor 2025-02-23 17:27:40 +00:00
Greg Johnston
848fd724dd Merge pull request #3644 from leptos-rs/nightly-ci-update 2025-02-22 15:19:44 -05:00
Greg Johnston
5bc254d49f chore: better way of not running whole doc tests 2025-02-22 14:29:25 -05:00
Greg Johnston
885f4a1654 chore: ignore type complexity lint 2025-02-22 08:39:00 -05:00
Greg Johnston
ddd243d07a chore: better way of not running whole doc tests 2025-02-22 08:38:39 -05:00
Greg Johnston
362c300eac chore: clippy doc comment indentation 2025-02-22 08:28:26 -05:00
Greg Johnston
284a724e5f chore(ci): update pinned nightly version 2025-02-22 07:35:09 -05:00
Evan Almloff
1a7b40b507 implement gloo websockets 2025-02-21 15:48:41 -06:00
Evan Almloff
73dd677843 switch to async fn for get implementation 2025-02-20 17:02:19 -06:00
Evan Almloff
131f414fdc move run client into the protocol trait 2025-02-20 17:01:51 -06:00
Evan Almloff
c3ed874d4d Create websocket protocol trait 2025-02-19 14:49:57 -06:00
benwis
f003e50446 Update throw_error to v0.3 2025-02-19 08:08:21 -08:00
Greg Johnston
ea29685c92 change: remove unused Result alias (#3543) 2025-02-19 07:22:20 -08:00
Greg Johnston
6ecc681cdd fix: allow decoding already-decoded URI components (closes #3606) (#3628)
* fix: allow decoding already-decoded URI components (closes #3606)

* clippy
2025-02-19 06:04:18 -08:00
zakstucke
49e44a2ec2 Erased routing, codegen opts (#3623) 2025-02-18 12:39:30 -08:00
Greg Johnston
7c34b4a4a5 fix: only render meta tags when rendered, not when created (closes #3629) (#3630) 2025-02-18 09:06:51 -05:00
Greg Johnston
7157958822 feat: support Option<_> in style: (closes #3568) (#3618) 2025-02-15 16:47:21 -05:00
martin frances
37cf25fba5 serde_json is common to (#3610)
integrations/actix
leptos/server
oco
server_fn

This is a defensive PR - Putting the crate definition into the root
workspcace makes it less likely to get difficult to trace version
slip bugs.

This does not help where sede_json is optional so care manual review
is required.
2025-02-15 10:24:07 -08:00
Greg Johnston
f975b8d33b fix: hydration of () (#3615) 2025-02-15 13:23:09 -05:00
zakstucke
d37450d12f Internally erase html elements (#3614) 2025-02-15 09:58:43 -08:00
Greg Johnston
4804dac32d chore: update either_of minimum version in workspace (#3612) 2025-02-15 08:19:05 -05:00
martin frances
a9f27d6128 chore: update syntax in README example (#3611) 2025-02-15 08:18:48 -05:00
Greg Johnston
04cb036a7d fix: reorder pause check in new_isomorphic (#3613) 2025-02-15 08:18:23 -05:00
jasper
1d3784ed7b feat: impl Into<Signal> for store subfields (#3579) 2025-02-14 17:14:49 -05:00
Greg Johnston
8cc1a34c00 feat: allow pausing and resuming effects (#3599) 2025-02-14 14:48:27 -05:00
mahdi739
68f4d46c5f feat: add invert to OptionStoreExt (#3534) 2025-02-14 13:52:38 -05:00
martin frances
590728e47e projects/bevy3d_ui: Bevy - Bugfix, clippy and crate bump (#3603)
The bugfix is related to access to a signal

"
bevy3d_ui-b20a0a6a298e7144.js:2637 At src/routes/demo1.rs:24:23, you access a
reactive_graph::signal::read::ReadSignal<bevy3d_ui::demos::bevydemo1::scene::Scene>
(defined at src/routes/demo1.rs:19:39) outside a reactive tracking context.
This might mean your app is not responding to changes in signal values in the way you expect.
"

The solution is to use .get_untracked() inside an "effect" block.

Lots of small clippy fixes.

Also here are the crates that have been bumped

-wasm-bindgen = "0.2.92"
-wasm-bindgen-test = "0.3.42"
-web-sys = "0.3.69"
+wasm-bindgen = "0.2.100"
+wasm-bindgen-test = "0.3.50"
+web-sys = "0.3.77"
2025-02-13 15:37:23 -08:00
martin frances
e84b527743 Minor: Bump tokio to 1.43. (#3600) 2025-02-12 23:14:00 -08:00
martin frances
96b125d54f Remove getrandom (#3589)
* Resolved this warning see while running cargo outdated

warning: Feature js of package getrandom has been obsolete in version 0.3.1

Now using the feature "wasm_js"

* the crate "getrandom" needs special configuration.

* getrandom_backend - a more generic config.

* Removed the crate getrandom
2025-02-12 23:13:27 -08:00
martin frances
16d66362f8 Minor: "wasm-bindgen" - Moved the crate definition up to the root workspace (#3588)
* Minor: "wasm-bindgen" - Moved the crate definition up to the root workspace

This synchronizes the version number amongst all sub-projects.
[Where the definition is "optional" manual adjustment is still required]

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-02-12 12:25:22 -08:00
martin frances
e27801a2c2 Minor: Bumped version of convert_case to 0.7 (#3590) 2025-02-12 12:17:36 -08:00
martin frances
b81f71997b Minor: Bump itertools to "0.14.0" (#3593)
The crate is used by "leptos_macro","reactive_stores" and "tachys"

So the definition of itertools can be centralized up into the root workspace
2025-02-12 12:16:47 -08:00
martin frances
2a11325749 Minor: leptos_config - Bump the "config" crate to version 0.15.8 (#3594) 2025-02-12 12:15:57 -08:00
martin frances
5604f3e979 projects/bevy3d_ui: Bevy migration (#3597)
-bevy = "0.14.1"
+bevy = "0.15.2"

<https://bevyengine.org/learn/migration-guides/0-14-to-0-15/>
2025-02-12 12:14:33 -08:00
martin frances
3a9a0891a3 projects/bevy3d_ui Migrate to leptos 0.7.7 (#3596)
* projects/bevy3d_ui Migrate to leptos 0.7.7

* Minor: projects/bevy3d_ui - Better Meta.
2025-02-12 12:13:32 -08:00
Greg Johnston
a39add50c0 fix: occasional use-after-disposed panic in Suspense (#3595) 2025-02-12 14:59:44 -05:00
Greg Johnston
2a2675dd36 fix: remove extra placeholder in Vec of text nodes (closes #3583) (#3592) 2025-02-12 10:45:56 -05:00
zakstucke
6ad300c592 Binary size wins (#3566)
* Random binary size opts

* [autofix.ci] apply automated fixes

* Stop autofixer removing an import

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-02-10 15:18:26 -08:00
zakstucke
b4e683d969 RenderHtml::into_owned (#3580) 2025-02-10 12:53:33 -08:00
Greg Johnston
c2289b23a7 fix: Actix stream error handling with 0.8 error types (#3574) 2025-02-10 08:32:44 -05:00
Greg Johnston
299acd25f3 implement AddAnyAttr for AnyView (#3562) 2025-02-09 20:46:41 -05:00
zakstucke
287fc47163 "Update axum paths to 0.8 syntax" (#3555)
* Update axum paths to 0.8 syntax

* [autofix.ci] apply automated fixes

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

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-02-06 16:17:12 -08:00
zakstucke
8f74a6d8a0 AddAnyAttr static (#3553) 2025-02-06 14:00:25 -08:00
starmaker
597175a54b Fixing closing brace (#3539)
Most likely confglict merge artefact
2025-02-01 09:40:03 -08:00
Chris
ede25b9e3d fix: remove Default impl for LeptosOptions and ConfFile (#3522)
* fix: remove `Default` impl for `LeptosOptions` and `ConfFile`

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-02-01 09:41:44 -05:00
Greg Johnston
8f636e354a change: allow IntoFuture for Suspend::new() (closes #3509) (#3532) 2025-01-31 14:25:35 -05:00
Greg Johnston
7da64f22c4 Merge branch 'main' into leptos_0.8 2025-01-30 21:32:58 -05:00
Greg Johnston
0073ae7d8a chore: update version numbers preparing for 0.8.0-alpha 2025-01-30 21:29:54 -05:00
benwis
8465716a19 Fix formatting 2025-01-26 09:51:22 -08:00
zakstucke
0e24b2e63f AddAnyAttr working with erase_components (#3518)
* AddAnyAttr working with erase_components

* CI fixes
2025-01-26 09:51:22 -08:00
Danik Vitek
c64d205984 feat (either_of): Extent API; Implement other iterator methods; Update deps (#3478)
* Implement other iterator methods. Update deps

* Formatting

* Update Cargo.lock

* [autofix.ci] apply automated fixes

* Formatting

* Move `Either` declaration into the `tuples` macro

* Comment out non-MSRV-compliant methods

* [autofix.ci] apply automated fixes

* Formatting

* Implement mapping functions

* Fix clippy warnings

* Impl `Error`; Impl `From<Result<A, B>> for Either<B, A>`

* Fix `Error` impl

* Move `Error` impl under `#[cfg(not(feature="no_std"))] until MSRV >= 1.81

* [autofix.ci] apply automated fixes

* Make `From<Result>` compliant with `EitherOr`. Add `impl EitherOr for Either`

* fix: use fully-qualified name

* fix: `EitherOf` test

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-01-26 09:51:22 -08:00
Greg Johnston
f17cb98eb0 chore: update workspace dependency versions to latest (#3506) 2025-01-26 09:51:22 -08:00
Danik Vitek
30f3e82664 docs: Fix README.md & Add MSRV badge (#3480)
* Fix README.md

* Add MSRV badge

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-01-26 09:51:22 -08:00
starmaker
152d5a5c92 issue-3467 - bumping codee version to support rkyv 8 (#3504)
* issue-3467 - bumping codee version to support rkyv 8

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-01-26 09:51:22 -08:00
Sam Jude
669e1ba7fa hexagonal architecture (#3342)
Co-authored-by: Sam <@>
2025-01-26 09:51:22 -08:00
dcsturman
2ad6a086f9 Enhanced docs for reactive_stores (#3508)
Added docs on shadow traits, Option, Enum, Vec, and Box usage with Store.
2025-01-26 09:51:22 -08:00
Greg Johnston
32e58d6b66 fix: including node_ref after {..} on arbitrary components (#3503) 2025-01-26 09:51:22 -08:00
Greg Johnston
a107443104 chore(ci): add CI for leptos_0.8 branch (#3500) 2025-01-26 09:51:22 -08:00
Greg Johnston
c859b07901 feat: #[lazy] macros to support lazy loading and code splitting (#3477) 2025-01-26 09:51:22 -08:00
Greg Johnston
a9868bea2b chore: restore reactivity warning at top level of components (closes #3354) (#3499) 2025-01-26 09:51:22 -08:00
Greg Johnston
7183c2b993 fix: correctly handle ErrorBoundary through reactive views (closes #3487) (#3492) 2025-01-26 09:51:22 -08:00
Greg Johnston
7a03621db1 feat: implement unboxing support for recursive store nodes (closes #3491) (#3493) 2025-01-26 09:51:22 -08:00
Spencer Ferris
2b589fa61f feat: Add more options for generating server fn routes (#3438)
* feat: Allow disabling server fn hash and customizing the default prefix

Allow configuring the default prefix for server function API routes. This is useful to
override the default prefix (`/api`) for all server functions without needing to manually
specify via `#[server(prefix = "...")]` on every server function.

Also, allow disabling appending the server functions' hashes to the end of their API names.
This is useful when an app's client side needs a stable server API. For example, shipping
the CSR WASM binary in a Tauri app. Tauri app releases are dependent on each platform's
distribution method (e.g., the Apple App Store or the Google Play Store), which typically
are much slower than the frequency at which a website can be updated. In addition, it's
common for users to not have the latest app version installed. In these cases, the CSR WASM
app would need to be able to continue calling the backend server function API, so the API
path needs to be consistent and not have a hash appended.

* Mark public structs as `#[non_exhaustive]` and add doc comments

* Minor refactor to pull the fn hash logic out of the `path` statement

* feat: Use module path in prefix for server fn API route

Allow including the module path of the server function in the API route. This
is an alternative strategy to prevent duplicate server function API routes
(the default strategy is to add a hash to the end of the route). Each element
of the module path will be separated by a `/`. For example, a server function
with a fully qualified name of `parent:🧒:server_fn` would have an API
route of `/api/parent/child/server_fn` (possibly with a different prefix and
a hash suffix depending on the values of the other server fn configs).

* Fix `enable_hash` if statement

* Add missing import
2025-01-24 20:11:38 -08:00
Saber Haj Rabiee
35e6f17930 chore: upgrade axum to v0.8 (#3439) 2025-01-17 13:38:37 -05:00
Greg Johnston
d1513a4a0b feat(breaking): allow make PossibleRouteMatch dyn-safe (#3421) 2025-01-17 13:33:00 -05:00
Mario Carbajal
aa27b9e474 feat: impl Dispose for Callback types and add try_run to the Callable trait (#3371)
* impl Dispose for Callback types and add try_run to the Callable trait

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-01-17 13:32:04 -05:00
Ryo Hirayama
cfe925d58f feat: allow any type that implements FromServerFnError as a replacement of the ServerFnError in server_fn (#3274) 2025-01-17 13:30:12 -05:00
215 changed files with 13646 additions and 5151 deletions

View File

@@ -28,5 +28,6 @@ jobs:
uses: ./.github/workflows/run-cargo-make-task.yml
with:
directory: ${{ matrix.directory }}
erased_mode: ${{ matrix.erased_mode }}
cargo_make_task: "ci"
toolchain: stable

View File

@@ -25,5 +25,6 @@ jobs:
uses: ./.github/workflows/run-cargo-make-task.yml
with:
directory: ${{ matrix.directory }}
erased_mode: ${{ matrix.erased_mode }}
cargo_make_task: "ci"
toolchain: stable

View File

@@ -18,7 +18,7 @@ jobs:
test:
needs: [get-leptos-changed]
if: needs.get-leptos-changed.outputs.leptos_changed == 'true' && github.event.pull_request.labels[0].name != 'breaking'
name: Run semver check (nightly-2024-08-01)
name: Run semver check (nightly-2025-03-05)
runs-on: ubuntu-latest
steps:
- name: Install Glib
@@ -30,4 +30,4 @@ jobs:
- name: Semver Checks
uses: obi1kenobi/cargo-semver-checks-action@v2
with:
rust-toolchain: nightly-2024-08-01
rust-toolchain: nightly-2025-03-05

View File

@@ -25,5 +25,6 @@ jobs:
uses: ./.github/workflows/run-cargo-make-task.yml
with:
directory: ${{ matrix.directory }}
erased_mode: ${{ matrix.erased_mode }}
cargo_make_task: "ci"
toolchain: nightly-2024-08-01
toolchain: nightly-2025-03-05

View File

@@ -50,5 +50,5 @@ jobs:
echo "matrix={\"directory\":${{ steps.changed-dirs.outputs.all_changed_files }}}" >> "$GITHUB_OUTPUT"
else
# Create matrix with one item to prevent an empty vector error
echo "matrix={\"directory\":[\"NO_CHANGE\"]}" >> "$GITHUB_OUTPUT"
echo "matrix={\"directory\":[\"NO_CHANGE\"], \"erased_mode\": [false, true]}" >> "$GITHUB_OUTPUT"
fi

View File

@@ -28,7 +28,7 @@ jobs:
sed 's/\/$//' |
jq -R -s -c 'split("\n")[:-1]')
echo "Example Directories: $examples"
echo "matrix={\"directory\":$examples}" >> "$GITHUB_OUTPUT"
echo "matrix={\"directory\":$examples, \"erased_mode\": [false, true]}" >> "$GITHUB_OUTPUT"
- name: Print Location Info
run: |
echo "Workspace: ${{ github.workspace }}"

View File

@@ -24,7 +24,7 @@ jobs:
sed "s|$(pwd)/||" |
jq -R -s -c 'split("\n")[:-1]')
echo "Leptos Directories: $crates"
echo "matrix={\"directory\":$crates}" >> "$GITHUB_OUTPUT"
echo "matrix={\"directory\":$crates, \"erased_mode\": [false, true]}" >> "$GITHUB_OUTPUT"
- name: Print Location Info
run: |
echo "Workspace: ${{ github.workspace }}"

View File

@@ -5,6 +5,9 @@ on:
directory:
required: true
type: string
erased_mode:
required: true
type: boolean
cargo_make_task:
required: true
type: string
@@ -15,9 +18,10 @@ env:
CARGO_TERM_COLOR: always
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
DEBIAN_FRONTEND: noninteractive
RUSTFLAGS: ${{ inputs.erased_mode && '--cfg erase_components' || '' }}
jobs:
test:
name: Run ${{ inputs.cargo_make_task }} (${{ inputs.toolchain }})
name: "Run ${{ inputs.cargo_make_task }} (${{ inputs.toolchain }}) (erased_mode: ${{ inputs.erased_mode }})"
runs-on: ubuntu-latest
steps:
- name: Free Disk Space

1105
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -40,36 +40,39 @@ members = [
exclude = ["benchmarks", "examples", "projects"]
[workspace.package]
version = "0.7.7"
version = "0.8.0-beta"
edition = "2021"
rust-version = "1.76"
[workspace.dependencies]
throw_error = { path = "./any_error/", version = "0.2.0" }
throw_error = { path = "./any_error/", version = "0.3.0" }
any_spawner = { path = "./any_spawner/", version = "0.2.0" }
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1" }
either_of = { path = "./either_of/", version = "0.1.0" }
hydration_context = { path = "./hydration_context", version = "0.2.0" }
leptos = { path = "./leptos", version = "0.7.7" }
leptos_config = { path = "./leptos_config", version = "0.7.7" }
leptos_dom = { path = "./leptos_dom", version = "0.7.7" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.7" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.7" }
leptos_macro = { path = "./leptos_macro", version = "0.7.7" }
leptos_router = { path = "./router", version = "0.7.7" }
leptos_router_macro = { path = "./router_macro", version = "0.7.7" }
leptos_server = { path = "./leptos_server", version = "0.7.7" }
leptos_meta = { path = "./meta", version = "0.7.7" }
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-beta" }
leptos_config = { path = "./leptos_config", version = "0.8.0-beta" }
leptos_dom = { path = "./leptos_dom", version = "0.8.0-beta" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.0-beta" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.8.0-beta" }
leptos_macro = { path = "./leptos_macro", version = "0.8.0-beta" }
leptos_router = { path = "./router", version = "0.8.0-beta" }
leptos_router_macro = { path = "./router_macro", version = "0.8.0-beta" }
leptos_server = { path = "./leptos_server", version = "0.8.0-beta" }
leptos_meta = { path = "./meta", version = "0.8.0-beta" }
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.1.7" }
reactive_stores = { path = "./reactive_stores", version = "0.1.7" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.7" }
server_fn = { path = "./server_fn", version = "0.7.7" }
server_fn_macro = { path = "./server_fn_macro", version = "0.7.7" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.7" }
tachys = { path = "./tachys", version = "0.1.7" }
reactive_graph = { path = "./reactive_graph", version = "0.2.0-beta" }
reactive_stores = { path = "./reactive_stores", version = "0.2.0-beta" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.2.0-beta" }
serde_json = "1.0.0"
server_fn = { path = "./server_fn", version = "0.8.0-beta" }
server_fn_macro = { path = "./server_fn_macro", version = "0.8.0-beta" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.0-beta" }
tachys = { path = "./tachys", version = "0.2.0-beta" }
wasm-bindgen = { version = "0.2.100" }
[profile.release]
codegen-units = 1

View File

@@ -21,7 +21,7 @@ use leptos::*;
#[component]
pub fn SimpleCounter(initial_value: i32) -> impl IntoView {
// create a reactive signal with the initial value
let (value, set_value) = create_signal(initial_value);
let (value, set_value) = signal(initial_value);
// create event handlers for our buttons
// note that `value` and `set_value` are `Copy`, so it's super easy to move them into closures
@@ -46,7 +46,7 @@ pub fn SimpleCounter(initial_value: i32) -> impl IntoView {
pub fn SimpleCounterWithBuilder(initial_value: i32) -> impl IntoView {
use leptos::html::*;
let (value, set_value) = create_signal(initial_value);
let (value, set_value) = signal(initial_value);
let clear = move |_| set_value(0);
let decrement = move |_| set_value.update(|value| *value -= 1);
let increment = move |_| set_value.update(|value| *value += 1);

View File

@@ -1,6 +1,6 @@
[package]
name = "throw_error"
version = "0.2.0"
version = "0.3.0"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -9,7 +9,7 @@ use std::{
error,
fmt::{self, Display},
future::Future,
mem, ops,
ops,
pin::Pin,
sync::Arc,
task::{Context, Poll},
@@ -17,11 +17,6 @@ use std::{
/* Wrapper Types */
/// This is a result type into which any error can be converted.
///
/// Results are stored as [`Error`].
pub type Result<T, E = Error> = core::result::Result<T, E>;
/// A generic wrapper for any error.
#[derive(Debug, Clone)]
#[repr(transparent)]
@@ -109,7 +104,7 @@ pub fn get_error_hook() -> Option<Arc<dyn ErrorHook>> {
/// Sets the current thread-local error hook, which will be invoked when [`throw`] is called.
pub fn set_error_hook(hook: Arc<dyn ErrorHook>) -> ResetErrorHookOnDrop {
ResetErrorHookOnDrop(
ERROR_HOOK.with_borrow_mut(|this| mem::replace(this, Some(hook))),
ERROR_HOOK.with_borrow_mut(|this| Option::replace(this, hook)),
)
}

View File

@@ -17,7 +17,7 @@ tokio = { version = "1.41", optional = true, default-features = false, features
"rt",
] }
tracing = { version = "0.1.41", optional = true }
wasm-bindgen-futures = { version = "0.4.47", optional = true }
wasm-bindgen-futures = { version = "0.4.50", optional = true }
[features]
async-executor = ["dep:async-executor"]

View File

@@ -23,7 +23,7 @@ tokio-test = "0.4.0"
miniserde = "0.1.0"
gloo = "0.8.0"
uuid = { version = "1.0", features = ["serde", "v4", "wasm-bindgen"] }
wasm-bindgen = "0.2.0"
wasm-bindgen = "0.2.100"
lazy_static = "1.0"
log = "0.4.0"
strum = "0.24.0"

View File

@@ -7,7 +7,7 @@ edition = "2021"
crate-type = ["cdylib", "rlib"]
[dependencies]
axum = { version = "0.7.5", optional = true }
axum = { version = "0.8.1", optional = true }
console_error_panic_hook = "0.1.7"
console_log = "1.0"
gloo-utils = "0.2.0"
@@ -20,18 +20,27 @@ leptos_axum = { path = "../../integrations/axum", optional = true }
leptos_router = { path = "../../router" }
serde = { version = "1.0", features = ["derive"] }
thiserror = "1.0"
tokio = { version = "1.39", features = [ "rt-multi-thread", "macros", "time" ], optional = true }
tokio = { version = "1.39", features = [
"rt-multi-thread",
"macros",
"time",
], optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
wasm-bindgen = "0.2.92"
web-sys = { version = "0.3.69", features = [ "AddEventListenerOptions", "Document", "Element", "Event", "EventListener", "EventTarget", "Performance", "Window" ], optional = true }
web-sys = { version = "0.3.69", features = [
"AddEventListenerOptions",
"Document",
"Element",
"Event",
"EventListener",
"EventTarget",
"Performance",
"Window",
], optional = true }
[features]
hydrate = [
"leptos/hydrate",
"dep:js-sys",
"dep:web-sys",
]
hydrate = ["leptos/hydrate", "dep:js-sys", "dep:web-sys"]
ssr = [
"dep:axum",
"dep:http-body-util",

View File

@@ -13,7 +13,7 @@ leptos_axum = { path = "../../integrations/axum", optional = true }
leptos_meta = { path = "../../meta" }
leptos_router = { path = "../../router" }
serde = { version = "1.0", features = ["derive"] }
axum = { version = "0.7.5", optional = true }
axum = { version = "0.8.1", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
tokio = { version = "1.39", features = ["full"], optional = true }

View File

@@ -45,7 +45,7 @@ async fn main() {
// build our application with a route
let app = Router::new()
.route("/special/:id", get(custom_handler))
.route("/special/{id}", get(custom_handler))
.leptos_routes(&leptos_options, routes, {
let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone())

View File

@@ -1,5 +1,4 @@
use leptos::prelude::*;
use leptos::tachys::html::style::style;
use serde::{Deserialize, Serialize};
use thiserror::Error;
@@ -16,7 +15,7 @@ pub enum CatError {
type CatCount = usize;
async fn fetch_cats(count: CatCount) -> Result<Vec<String>> {
async fn fetch_cats(count: CatCount) -> Result<Vec<String>, Error> {
if count > 0 {
gloo_timers::future::TimeoutFuture::new(1000).await;
// make the request
@@ -42,11 +41,7 @@ async fn fetch_cats(count: CatCount) -> Result<Vec<String>> {
pub fn fetch_example() -> impl IntoView {
let (cat_count, set_cat_count) = signal::<CatCount>(1);
// we use new_unsync here because the reqwasm request type isn't Send
// if we were doing SSR, then
// 1) we'd want to use a Resource, so the data would be serialized to the client
// 2) we'd need to make sure there was a thread-local spawner set up
let cats = AsyncDerived::new_unsync(move || fetch_cats(cat_count.get()));
let cats = LocalResource::new(move || fetch_cats(cat_count.get()));
let fallback = move |errors: ArcRwSignal<Errors>| {
let error_list = move || {
@@ -66,8 +61,6 @@ pub fn fetch_example() -> impl IntoView {
}
};
let spreadable = style(("background-color", "AliceBlue"));
view! {
<div>
<label>
@@ -82,7 +75,7 @@ pub fn fetch_example() -> impl IntoView {
/>
</label>
<Transition fallback=|| view! { <div>"Loading..."</div> } {..spreadable}>
<Transition fallback=|| view! { <div>"Loading..."</div> }>
<ErrorBoundary fallback>
<ul>
{move || Suspend::new(async move {
@@ -92,7 +85,7 @@ pub fn fetch_example() -> impl IntoView {
.map(|s| {
view! {
<li>
<img src=s.clone()/>
<img src=s.clone() />
</li>
}
})

View File

@@ -20,7 +20,7 @@ serde = { version = "1.0", features = ["derive"] }
tracing = "0.1.40"
gloo-net = { version = "0.6.0", features = ["http"] }
reqwest = { version = "0.12.5", features = ["json"] }
axum = { version = "0.7.5", optional = true }
axum = { version = "0.8.1", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
tokio = { version = "1.39", features = ["full"], optional = true }

View File

@@ -20,7 +20,7 @@ serde = { version = "1.0", features = ["derive"] }
tracing = "0.1.40"
gloo-net = { version = "0.6.0", features = ["http"] }
reqwest = { version = "0.12.5", features = ["json"] }
axum = { version = "0.7.5", optional = true, features = ["http2"] }
axum = { version = "0.8.1", optional = true, features = ["http2"] }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", features = [
"fs",

View File

@@ -23,7 +23,7 @@ serde = { version = "1.0", features = ["derive"] }
tracing = "0.1.40"
gloo-net = { version = "0.6.0", features = ["http"] }
reqwest = { version = "0.12.5", features = ["json"] }
axum = { version = "0.7.5", default-features = false, optional = true }
axum = { version = "0.8.1", default-features = false, optional = true }
tower = { version = "0.4.13", optional = true }
http = { version = "1.1", optional = true }
web-sys = { version = "0.3.70", features = [

View File

@@ -10,15 +10,12 @@ 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"] }
server_fn = { path = "../../server_fn", features = ["serde-lite"] }
leptos_axum = { path = "../../integrations/axum", optional = true }
log = "0.4.22"
serde = { version = "1.0", features = ["derive"] }
axum = { version = "0.7.5", optional = true }
axum = { version = "0.8.1", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
tokio = { version = "1.39", features = ["full"], optional = true }

View File

@@ -10,22 +10,20 @@ 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"] }
leptos_router = { path = "../../router" }
server_fn = { path = "../../server_fn", features = ["serde-lite"] }
leptos_axum = { path = "../../integrations/axum", features = [
"dont-use-islands-router",
"islands-router",
], optional = true }
log = "0.4.22"
serde = { version = "1.0", features = ["derive"] }
axum = { version = "0.7.5", optional = true }
axum = { version = "0.8.1", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
tokio = { version = "1.39", features = ["full"], optional = true }
wasm-bindgen = "0.2.93"
wasm-bindgen = "0.2.100"
serde_json = "1.0.133"
[features]
hydrate = ["leptos/hydrate"]
@@ -58,11 +56,11 @@ site-root = "target/site"
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "./style.css"
style-file = "style.css"
# [Optional] Files in the asset-dir will be copied to the site-root directory
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "127.0.0.1:3000"
site-addr = "127.0.0.1:3009"
# The port to use for automatic reload monitoring
reload-port = 3001
# The browserlist query used for optimizing the CSS.

File diff suppressed because it is too large Load Diff

View File

@@ -1,140 +0,0 @@
window.addEventListener("click", async (ev) => {
// confirm that this is an <a> that meets our requirements
if (
ev.defaultPrevented ||
ev.button !== 0 ||
ev.metaKey ||
ev.altKey ||
ev.ctrlKey ||
ev.shiftKey
)
return;
/** @type HTMLAnchorElement | undefined;*/
const a = ev
.composedPath()
.find(el => el instanceof Node && el.nodeName.toUpperCase() === "A");
if (!a) return;
const svg = a.namespaceURI === "http://www.w3.org/2000/svg";
const href = svg ? a.href.baseVal : a.href;
const target = svg ? a.target.baseVal : a.target;
if (target || (!href && !a.hasAttribute("state"))) return;
const rel = (a.getAttribute("rel") || "").split(/\s+/);
if (a.hasAttribute("download") || (rel && rel.includes("external"))) return;
const url = svg ? new URL(href, document.baseURI) : new URL(href);
if (
url.origin !== window.location.origin // ||
// TODO base
//(basePath && url.pathname && !url.pathname.toLowerCase().startsWith(basePath.toLowerCase()))
)
return;
ev.preventDefault();
// fetch the new page
const resp = await fetch(url);
const htmlString = await resp.text();
// Use DOMParser to parse the HTML string
const parser = new DOMParser();
// TODO parse from the request stream instead?
const doc = parser.parseFromString(htmlString, 'text/html');
// The 'doc' variable now contains the parsed DOM
const transition = async () => {
const oldDocWalker = document.createTreeWalker(document);
const newDocWalker = doc.createTreeWalker(doc);
let oldNode = oldDocWalker.currentNode;
let newNode = newDocWalker.currentNode;
while(oldDocWalker.nextNode() && newDocWalker.nextNode()) {
oldNode = oldDocWalker.currentNode;
newNode = newDocWalker.currentNode;
// if the nodes are different, we need to replace the old with the new
// because of the typed view tree, this should never actually happen
if (oldNode.nodeType !== newNode.nodeType) {
oldNode.replaceWith(newNode);
}
// if it's a text node, just update the text with the new text
else if (oldNode.nodeType === Node.TEXT_NODE) {
oldNode.textContent = newNode.textContent;
}
// if it's an element, replace if it's a different tag, or update attributes
else if (oldNode.nodeType === Node.ELEMENT_NODE) {
/** @type Element */
const oldEl = oldNode;
/** @type Element */
const newEl = newNode;
if (oldEl.tagName !== newEl.tagName) {
oldEl.replaceWith(newEl);
}
else {
for(const attr of newEl.attributes) {
oldEl.setAttribute(attr.name, attr.value);
}
}
}
// we use comment "branch marker" nodes to distinguish between different branches in the statically-typed view tree
// if one of these marker is hit, then there are two options
// 1) it's the same branch, and we just keep walking until the end
// 2) it's a different branch, in which case the old can be replaced with the new wholesale
else if (oldNode.nodeType === Node.COMMENT_NODE) {
const oldText = oldNode.textContent;
const newText = newNode.textContent;
if(oldText.startsWith("bo") && newText !== oldText) {
oldDocWalker.nextNode();
newDocWalker.nextNode();
const oldRange = new Range();
const newRange = new Range();
let oldBranches = 1;
let newBranches = 1;
while(oldBranches > 0 && newBranches > 0) {
if(oldDocWalker.nextNode() && newDocWalker.nextNode()) {
console.log(oldDocWalker.currentNode, newDocWalker.currentNode);
if(oldDocWalker.currentNode.nodeType === Node.COMMENT_NODE) {
if(oldDocWalker.currentNode.textContent.startsWith("bo")) {
oldBranches += 1;
} else if(oldDocWalker.currentNode.textContent.startsWith("bc")) {
oldBranches -= 1;
}
}
if(newDocWalker.currentNode.nodeType === Node.COMMENT_NODE) {
if(newDocWalker.currentNode.textContent.startsWith("bo")) {
newBranches += 1;
} else if(newDocWalker.currentNode.textContent.startsWith("bc")) {
newBranches -= 1;
}
}
}
}
try {
oldRange.setStartAfter(oldNode);
oldRange.setEndBefore(oldDocWalker.currentNode);
newRange.setStartAfter(newNode);
newRange.setEndBefore(newDocWalker.currentNode);
const newContents = newRange.extractContents();
oldRange.deleteContents();
oldRange.insertNode(newContents);
oldNode.replaceWith(newNode);
oldDocWalker.currentNode.replaceWith(newDocWalker.currentNode);
} catch (e) {
console.error(e);
}
} }
}
};
// Not all browsers support startViewTransition; see https://caniuse.com/?search=startViewTransition
if (document.startViewTransition) {
await document.startViewTransition(transition);
} else {
await transition()
}
window.history.pushState(undefined, null, url);
});

View File

@@ -1,8 +1,13 @@
use leptos::prelude::*;
use leptos_router::{
components::{FlatRoutes, Route, Router},
StaticSegment,
use leptos::{
either::{Either, EitherOf3},
prelude::*,
};
use leptos_router::{
components::{Route, Router, Routes},
hooks::{use_params_map, use_query_map},
path,
};
use serde::{Deserialize, Serialize};
pub fn shell(options: LeptosOptions) -> impl IntoView {
view! {
@@ -12,7 +17,7 @@ pub fn shell(options: LeptosOptions) -> impl IntoView {
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<AutoReload options=options.clone()/>
<HydrationScripts options=options islands=true/>
<HydrationScripts options=options islands=true islands_router=true/>
<link rel="stylesheet" id="leptos" href="/pkg/islands.css"/>
<link rel="shortcut icon" type="image/ico" href="/favicon.ico"/>
</head>
@@ -26,34 +31,180 @@ pub fn shell(options: LeptosOptions) -> impl IntoView {
#[component]
pub fn App() -> impl IntoView {
view! {
<script src="/routing.js"></script>
<Router>
<header>
<h1>"My Application"</h1>
<h1>"My Contacts"</h1>
</header>
<nav>
<a href="/">"Page A"</a>
<a href="/b">"Page B"</a>
<a href="/">"Home"</a>
<a href="/about">"About"</a>
</nav>
<main>
<p>
<label>"Home Checkbox" <input type="checkbox"/></label>
</p>
<FlatRoutes fallback=|| "Not found.">
<Route path=StaticSegment("") view=PageA/>
<Route path=StaticSegment("b") view=PageB/>
</FlatRoutes>
<Routes fallback=|| "Not found.">
<Route path=path!("") view=Home/>
<Route path=path!("user/:id") view=Details/>
<Route path=path!("about") view=About/>
</Routes>
</main>
</Router>
}
}
#[component]
pub fn PageA() -> impl IntoView {
view! { <label>"Page A" <input type="checkbox"/></label> }
#[server]
pub async fn search(query: String) -> Result<Vec<User>, ServerFnError> {
let users = tokio::fs::read_to_string("./mock_data.json").await?;
let data: Vec<User> = serde_json::from_str(&users)?;
let query = query.to_ascii_lowercase();
Ok(data
.into_iter()
.filter(|user| {
user.first_name.to_ascii_lowercase().contains(&query)
|| user.last_name.to_ascii_lowercase().contains(&query)
|| user.email.to_ascii_lowercase().contains(&query)
})
.collect())
}
#[server]
pub async fn delete_user(id: u32) -> Result<(), ServerFnError> {
let users = tokio::fs::read_to_string("./mock_data.json").await?;
let mut data: Vec<User> = serde_json::from_str(&users)?;
data.retain(|user| user.id != id);
let new_json = serde_json::to_string(&data)?;
tokio::fs::write("./mock_data.json", &new_json).await?;
Ok(())
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct User {
id: u32,
first_name: String,
last_name: String,
email: String,
}
#[component]
pub fn PageB() -> impl IntoView {
view! { <label>"Page B" <input type="checkbox"/></label> }
pub fn Home() -> impl IntoView {
let q = use_query_map();
let q = move || q.read().get("q");
let data = Resource::new(q, |q| async move {
if let Some(q) = q {
search(q).await
} else {
Ok(vec![])
}
});
let delete_user_action = ServerAction::<DeleteUser>::new();
let view = move || {
Suspend::new(async move {
let users = data.await.unwrap();
if q().is_none() {
EitherOf3::A(view! {
<p class="note">"Enter a search to begin viewing contacts."</p>
})
} else if users.is_empty() {
EitherOf3::B(view! {
<p class="note">"No users found matching that search."</p>
})
} else {
EitherOf3::C(view! {
<table>
<tbody>
<For
each=move || users.clone()
key=|user| user.id
let:user
>
<tr>
<td>{user.first_name}</td>
<td>{user.last_name}</td>
<td>{user.email}</td>
<td>
<a href=format!("/user/{}", user.id)>"Details"</a>
<input type="checkbox"/>
<ActionForm action=delete_user_action>
<input type="hidden" name="id" value=user.id/>
<input type="submit" value="Delete"/>
</ActionForm>
</td>
</tr>
</For>
</tbody>
</table>
})
}
})
};
view! {
<section class="page">
<form method="GET" class="search">
<input type="search" name="q" value=q autofocus oninput="this.form.requestSubmit()"/>
<input type="submit"/>
</form>
<Suspense fallback=|| view! { <p>"Loading..."</p> }>{view}</Suspense>
</section>
}
}
#[component]
pub fn Details() -> impl IntoView {
#[server]
pub async fn get_user(id: u32) -> Result<Option<User>, ServerFnError> {
let users = tokio::fs::read_to_string("./mock_data.json").await?;
let data: Vec<User> = serde_json::from_str(&users)?;
Ok(data.iter().find(|user| user.id == id).cloned())
}
let params = use_params_map();
let id = move || {
params
.read()
.get("id")
.and_then(|id| id.parse::<u32>().ok())
};
let user = Resource::new(id, |id| async move {
match id {
None => Ok(None),
Some(id) => get_user(id).await,
}
});
move || {
Suspend::new(async move {
user.await.map(|user| match user {
None => Either::Left(view! {
<section class="page">
<h2>"Not found."</h2>
<p>"Sorry — we couldnt find that user."</p>
</section>
}),
Some(user) => Either::Right(view! {
<section class="page">
<h2>{user.first_name} " " { user.last_name}</h2>
<p class="email">{user.email}</p>
</section>
}),
})
})
}
}
#[component]
pub fn About() -> impl IntoView {
view! {
<section class="page">
<h2>"About"</h2>
<p>"This demo is intended to show off an experimental “islands router” feature, which mimics the smooth transitions and user experience of client-side routing while minimizing the amount of code that actually runs in the browser."</p>
<p>"By default, all the content in this application is only rendered on the server. But you can add client-side interactivity via islands like this one:"</p>
<Counter/>
</section>
}
}
#[island]
pub fn Counter() -> impl IntoView {
let count = RwSignal::new(0);
view! {
<button class="counter" on:click=move |_| *count.write() += 1>{count}</button>
}
}

View File

@@ -1,3 +1,52 @@
.pending {
color: purple;
body {
font-family: system-ui, sans-serif;
background-color: #f6f6fa;
}
h1, h2, h3, h4, h5, h6 {
font-family: ui-rounded, 'Hiragino Maru Gothic ProN', Quicksand, Comfortaa, Manjari, 'Arial Rounded MT', 'Arial Rounded MT Bold', Calibri, source-sans-pro, sans-serif;
text-align: center;
}
nav {
padding: 1rem;
text-align: center;
}
nav a {
margin: 1rem;
}
form.search {
display: flex;
margin: 2rem auto;
justify-content: center;
}
td {
min-width: 10rem;
width: 10rem;
}
table {
min-width: 100%;
}
.page {
width: 80%;
margin: auto;
}
td:last-child > * {
display: inline-block;
}
.note, .note {
text-align: center;
}
button.counter {
display: block;
font-size: 2rem;
margin: auto;
}

View File

@@ -149,12 +149,12 @@ pub fn App() -> impl IntoView {
</div>
<div class="col-md-6">
<div class="row">
<Button id="run" text="Create 1,000 rows" on:click=run/>
<Button id="runlots" text="Create 10,000 rows" on:click=run_lots/>
<Button id="add" text="Append 1,000 rows" on:click=add/>
<Button id="update" text="Update every 10th row" on:click=update/>
<Button id="clear" text="Clear" on:click=clear/>
<Button id="swaprows" text="Swap Rows" on:click=swap_rows/>
<Button id="run" text="Create 1,000 rows" on:click=run />
<Button id="runlots" text="Create 10,000 rows" on:click=run_lots />
<Button id="add" text="Append 1,000 rows" on:click=add />
<Button id="update" text="Update every 10th row" on:click=update />
<Button id="clear" text="Clear" on:click=clear />
<Button id="swaprows" text="Swap Rows" on:click=swap_rows />
</div>
</div>
</div>

View File

@@ -9,7 +9,6 @@ use leptos_router::{
},
hooks::{use_navigate, use_params, use_query_map},
params::Params,
MatchNestedRoutes,
};
use leptos_router_macro::path;
use std::time::Duration;
@@ -25,7 +24,7 @@ pub fn RouterExample() -> impl IntoView {
// contexts are passed down through the route tree
provide_context(ExampleContext(0));
// this signal will be ued to set whether we are allowed to access a protected route
// this signal will be used to set whether we are allowed to access a protected route
let (logged_in, set_logged_in) = signal(true);
let (is_routing, set_is_routing) = signal(false);
@@ -33,7 +32,7 @@ pub fn RouterExample() -> impl IntoView {
<Router set_is_routing>
// shows a progress bar while async data are loading
<div class="routing-progress">
<RoutingProgress is_routing max_time=Duration::from_millis(250)/>
<RoutingProgress is_routing max_time=Duration::from_millis(250) />
</div>
<nav>
// ordinary <a> elements can be used for client-side navigation
@@ -53,15 +52,15 @@ pub fn RouterExample() -> impl IntoView {
<Routes transition=true fallback=|| "This page could not be found.">
// paths can be created using the path!() macro, or provided as types like
// StaticSegment("about")
<Route path=path!("about") view=About/>
<Route path=path!("about") view=About />
<ProtectedRoute
path=path!("settings")
condition=move || Some(logged_in.get())
redirect_path=|| "/"
view=Settings
/>
<Route path=path!("redirect-home") view=|| view! { <Redirect path="/"/> }/>
<ContactRoutes/>
<Route path=path!("redirect-home") view=|| view! { <Redirect path="/" /> } />
<ContactRoutes />
</Routes>
</main>
</Router>
@@ -71,11 +70,11 @@ pub fn RouterExample() -> impl IntoView {
// You can define other routes in their own component.
// Routes implement the MatchNestedRoutes
#[component(transparent)]
pub fn ContactRoutes() -> impl MatchNestedRoutes + Clone {
pub fn ContactRoutes() -> impl leptos_router::MatchNestedRoutes + Clone {
view! {
<ParentRoute path=path!("") view=ContactList>
<Route path=path!("/") view=|| "Select a contact."/>
<Route path=path!("/:id") view=Contact/>
<Route path=path!("/") view=|| "Select a contact." />
<Route path=path!("/:id") view=Contact />
</ParentRoute>
}
.into_inner()
@@ -122,7 +121,7 @@ pub fn ContactList() -> impl IntoView {
<Suspense fallback=move || view! { <p>"Loading contacts..."</p> }>
<ul>{contacts}</ul>
</Suspense>
<Outlet/>
<Outlet />
</div>
}
}
@@ -166,7 +165,7 @@ pub fn Contact() -> impl IntoView {
Some(contact) => Either::Right(view! {
<section class="card">
<h1>{contact.first_name} " " {contact.last_name}</h1>
<p>{contact.address_1} <br/> {contact.address_2}</p>
<p>{contact.address_1} <br /> {contact.address_2}</p>
</section>
}),
}
@@ -224,10 +223,10 @@ pub fn Settings() -> impl IntoView {
<Form action="">
<fieldset>
<legend>"Name"</legend>
<input type="text" name="first_name" placeholder="First"/>
<input type="text" name="last_name" placeholder="Last"/>
<input type="text" name="first_name" placeholder="First" />
<input type="text" name="last_name" placeholder="Last" />
</fieldset>
<input type="submit"/>
<input type="submit" />
<p>
"This uses the " <code>"<Form/>"</code>
" component, which enhances forms by using client-side navigation for "

View File

@@ -21,21 +21,21 @@ server_fn = { path = "../../server_fn", features = [
log = "0.4.22"
simple_logger = "5.0"
serde = { version = "1.0", features = ["derive"] }
axum = { version = "0.7.5", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", features = [
axum = { version = "0.8.1", optional = true }
tower = { version = "0.5.2", optional = true }
tower-http = { version = "0.6.2", features = [
"fs",
"tracing",
"trace",
], optional = true }
tokio = { version = "1.39", features = ["full"], optional = true }
thiserror = "1.0"
thiserror = "2.0.11"
wasm-bindgen = "0.2.93"
serde_toml = "0.0.1"
toml = "0.8.19"
web-sys = { version = "0.3.70", features = ["FileList", "File"] }
strum = { version = "0.26.3", features = ["strum_macros", "derive"] }
notify = { version = "6.1", optional = true }
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 }

View File

@@ -1,4 +1,4 @@
use futures::StreamExt;
use futures::{Sink, Stream, StreamExt};
use http::Method;
use leptos::{html::Input, prelude::*, task::spawn_local};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
@@ -9,8 +9,10 @@ use server_fn::{
MultipartFormData, Postcard, Rkyv, SerdeLite, StreamingText,
TextStream,
},
error::{FromServerFnError, IntoAppError, ServerFnErrorErr},
request::{browser::BrowserRequest, ClientReq, Req},
response::{browser::BrowserResponse, ClientRes, Res},
response::{browser::BrowserResponse, ClientRes, TryRes},
ContentType,
};
use std::future::Future;
#[cfg(feature = "ssr")]
@@ -652,32 +654,72 @@ pub fn FileWatcher() -> impl IntoView {
/// implementations if you'd like. However, it's much lighter weight to use something like `strum`
/// simply to generate those trait implementations.
#[server]
pub async fn ascii_uppercase(
text: String,
) -> Result<String, ServerFnError<InvalidArgument>> {
pub async fn ascii_uppercase(text: String) -> Result<String, MyErrors> {
other_error()?;
Ok(ascii_uppercase_inner(text)?)
}
pub fn other_error() -> Result<(), String> {
Ok(())
}
pub fn ascii_uppercase_inner(text: String) -> Result<String, InvalidArgument> {
if text.len() < 5 {
Err(InvalidArgument::TooShort.into())
Err(InvalidArgument::TooShort)
} else if text.len() > 15 {
Err(InvalidArgument::TooLong.into())
Err(InvalidArgument::TooLong)
} else if text.is_ascii() {
Ok(text.to_ascii_uppercase())
} else {
Err(InvalidArgument::NotAscii.into())
Err(InvalidArgument::NotAscii)
}
}
#[server]
pub async fn ascii_uppercase_classic(
text: String,
) -> Result<String, ServerFnError<InvalidArgument>> {
Ok(ascii_uppercase_inner(text)?)
}
// The EnumString and Display derive macros are provided by strum
#[derive(Debug, Clone, EnumString, Display)]
#[derive(Debug, Clone, Display, EnumString, Serialize, Deserialize)]
pub enum InvalidArgument {
TooShort,
TooLong,
NotAscii,
}
#[derive(Debug, Clone, Display, Serialize, Deserialize)]
pub enum MyErrors {
InvalidArgument(InvalidArgument),
ServerFnError(ServerFnErrorErr),
Other(String),
}
impl From<InvalidArgument> for MyErrors {
fn from(value: InvalidArgument) -> Self {
MyErrors::InvalidArgument(value)
}
}
impl From<String> for MyErrors {
fn from(value: String) -> Self {
MyErrors::Other(value)
}
}
impl FromServerFnError for MyErrors {
fn from_server_fn_error(value: ServerFnErrorErr) -> Self {
MyErrors::ServerFnError(value)
}
}
#[component]
pub fn CustomErrorTypes() -> impl IntoView {
let input_ref = NodeRef::<Input>::new();
let (result, set_result) = signal(None);
let (result_classic, set_result_classic) = signal(None);
view! {
<h3>Using custom error types</h3>
@@ -692,14 +734,17 @@ pub fn CustomErrorTypes() -> impl IntoView {
<button on:click=move |_| {
let value = input_ref.get().unwrap().value();
spawn_local(async move {
let data = ascii_uppercase(value).await;
let data = ascii_uppercase(value.clone()).await;
let data_classic = ascii_uppercase_classic(value).await;
set_result.set(Some(data));
set_result_classic.set(Some(data_classic));
});
}>
"Submit"
</button>
<p>{move || format!("{:?}", result.get())}</p>
<p>{move || format!("{:?}", result_classic.get())}</p>
}
}
@@ -717,8 +762,11 @@ pub struct Toml;
#[derive(Serialize, Deserialize)]
pub struct TomlEncoded<T>(T);
impl Encoding for Toml {
impl ContentType for Toml {
const CONTENT_TYPE: &'static str = "application/toml";
}
impl Encoding for Toml {
const METHOD: Method = Method::POST;
}
@@ -726,14 +774,12 @@ impl<T, Request, Err> IntoReq<Toml, Request, Err> for TomlEncoded<T>
where
Request: ClientReq<Err>,
T: Serialize,
Err: FromServerFnError,
{
fn into_req(
self,
path: &str,
accepts: &str,
) -> Result<Request, ServerFnError<Err>> {
let data = toml::to_string(&self.0)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
fn into_req(self, path: &str, accepts: &str) -> Result<Request, Err> {
let data = toml::to_string(&self.0).map_err(|e| {
ServerFnErrorErr::Serialization(e.to_string()).into_app_error()
})?;
Request::try_new_post(path, Toml::CONTENT_TYPE, accepts, data)
}
}
@@ -742,23 +788,26 @@ impl<T, Request, Err> FromReq<Toml, Request, Err> for TomlEncoded<T>
where
Request: Req<Err> + Send,
T: DeserializeOwned,
Err: FromServerFnError,
{
async fn from_req(req: Request) -> Result<Self, ServerFnError<Err>> {
async fn from_req(req: Request) -> Result<Self, Err> {
let string_data = req.try_into_string().await?;
toml::from_str::<T>(&string_data)
.map(TomlEncoded)
.map_err(|e| ServerFnError::Args(e.to_string()))
.map_err(|e| ServerFnErrorErr::Args(e.to_string()).into_app_error())
}
}
impl<T, Response, Err> IntoRes<Toml, Response, Err> for TomlEncoded<T>
where
Response: Res<Err>,
Response: TryRes<Err>,
T: Serialize + Send,
Err: FromServerFnError,
{
async fn into_res(self) -> Result<Response, ServerFnError<Err>> {
let data = toml::to_string(&self.0)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
async fn into_res(self) -> Result<Response, Err> {
let data = toml::to_string(&self.0).map_err(|e| {
ServerFnErrorErr::Serialization(e.to_string()).into_app_error()
})?;
Response::try_from_string(Toml::CONTENT_TYPE, data)
}
}
@@ -767,12 +816,13 @@ impl<T, Response, Err> FromRes<Toml, Response, Err> for TomlEncoded<T>
where
Response: ClientRes<Err> + Send,
T: DeserializeOwned,
Err: FromServerFnError,
{
async fn from_res(res: Response) -> Result<Self, ServerFnError<Err>> {
async fn from_res(res: Response) -> Result<Self, Err> {
let data = res.try_into_string().await?;
toml::from_str(&data)
.map(TomlEncoded)
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
toml::from_str(&data).map(TomlEncoded).map_err(|e| {
ServerFnErrorErr::Deserialization(e.to_string()).into_app_error()
})
}
}
@@ -835,7 +885,10 @@ pub fn CustomClientExample() -> impl IntoView {
pub struct CustomClient;
// Implement the `Client` trait for it.
impl<CustErr> Client<CustErr> for CustomClient {
impl<E> Client<E> for CustomClient
where
E: FromServerFnError,
{
// BrowserRequest and BrowserResponse are the defaults used by other server functions.
// They are wrappers for the underlying Web Fetch API types.
type Request = BrowserRequest;
@@ -844,8 +897,7 @@ pub fn CustomClientExample() -> impl IntoView {
// Our custom `send()` implementation does all the work.
fn send(
req: Self::Request,
) -> impl Future<Output = Result<Self::Response, ServerFnError<CustErr>>>
+ Send {
) -> impl Future<Output = Result<Self::Response, E>> + Send {
// BrowserRequest derefs to the underlying Request type from gloo-net,
// so we can get access to the headers here
let headers = req.headers();
@@ -854,6 +906,24 @@ pub fn CustomClientExample() -> impl IntoView {
// delegate back out to BrowserClient to send the modified request
BrowserClient::send(req)
}
fn open_websocket(
path: &str,
) -> impl Future<
Output = Result<
(
impl Stream<Item = Result<server_fn::Bytes, E>> + Send + 'static,
impl Sink<Result<server_fn::Bytes, E>> + Send + 'static,
),
E,
>,
> + Send {
BrowserClient::open_websocket(path)
}
fn spawn(future: impl Future<Output = ()> + Send + 'static) {
<BrowserClient as Client<E>>::spawn(future)
}
}
// Specify our custom client with `client = `

View File

@@ -1,2 +1,2 @@
[toolchain]
channel = "nightly-2024-01-29"
channel = "nightly-2025-03-05"

View File

@@ -20,7 +20,7 @@ leptos_router = { path = "../../router" }
log = "0.4.22"
serde = { version = "1.0", features = ["derive"] }
thiserror = "1.0"
axum = { version = "0.7.5", optional = true }
axum = { version = "0.8.1", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
tokio = { version = "1.39", features = [

View File

@@ -18,7 +18,7 @@ leptos_router = { path = "../../router" }
log = "0.4.22"
serde = { version = "1.0", features = ["derive"] }
thiserror = "1.0"
axum = { version = "0.7.5", optional = true }
axum = { version = "0.8.1", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
tokio = { version = "1.39", features = [
@@ -45,7 +45,7 @@ ssr = [
"dep:leptos_axum",
"leptos_router/ssr",
"dep:notify",
"dep:http"
"dep:http",
]
[profile.release]

View File

@@ -0,0 +1,99 @@
@check_instrumented_issue_3719
Feature: Using instrumented counters to test regression from #3502.
Check that the suspend/suspense and the underlying resources are
called with the expected number of times. If this was already in
place by #3502 (5c43c18) it should have caught this regression.
For a better minimum demonstration see #3719.
Background:
Given I see the app
And I select the mode Instrumented
Scenario: follow all paths via CSR avoids #3502
Given I select the following links
| Item Listing |
| Item 1 |
| Inspect path2 |
| Inspect path2/field3 |
And I click on Reset CSR Counters
When I select the following links
| Inspect path2/field1 |
| Inspect path2/field2 |
And I go check the Counters
Then I see the following counters under section
| Suspend Calls | |
| item_listing | 0 |
| item_overview | 0 |
| item_inspect | 2 |
And the following counters under section
| Server Calls (CSR) | |
| list_items | 0 |
| get_item | 0 |
| inspect_item_root | 0 |
| inspect_item_field | 2 |
# To show that starting directly from within a param will simply
# cause the problem.
Scenario: Quicker way to demonstrate regression caused by #3502
Given I select the link Target 123
# And I click on Reset CSR Counters
When I select the following links
| Inspect path2/field1 |
| Inspect path2/field2 |
And I go check the Counters
Then I see the following counters under section
| Suspend Calls | |
| item_listing | 0 |
| item_overview | 0 |
| item_inspect | 3 |
And the following counters under section
| Server Calls (CSR) | |
| list_items | 1 |
| get_item | 1 |
| inspect_item_root | 0 |
| inspect_item_field | 4 |
Scenario: Follow paths ordinarily down to a target
Given I select the following links
| Item Listing |
| Item 1 |
And I click on Reset CSR Counters
When I select the following links
| Target 4## |
| Target 3## |
And I go check the Counters
Then I see the following counters under section
| Suspend Calls | |
| item_listing | 0 |
| item_overview | 2 |
| item_inspect | 0 |
And the following counters under section
| Server Calls (CSR) | |
| list_items | 0 |
| get_item | 2 |
| inspect_item_root | 0 |
| inspect_item_field | 0 |
Scenario: Same as above, but add a refresh to test hydration
Given I select the following links
| Item Listing |
| Item 1 |
And I refresh the page
And I click on Reset CSR Counters
When I select the following links
| Target 4## |
| Target 3## |
And I go check the Counters
Then I see the following counters under section
| Suspend Calls | |
| item_listing | 0 |
| item_overview | 2 |
| item_inspect | 0 |
And the following counters under section
| Server Calls (CSR) | |
| list_items | 0 |
| get_item | 2 |
| inspect_item_root | 0 |
| inspect_item_field | 0 |

View File

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

View File

@@ -4,7 +4,7 @@ use leptos_router::{
hooks::use_params,
nested_router::Outlet,
params::Params,
MatchNestedRoutes, ParamSegment, SsrMode, StaticSegment, WildcardSegment,
ParamSegment, SsrMode, StaticSegment, WildcardSegment,
};
#[cfg(feature = "ssr")]
@@ -21,6 +21,7 @@ pub(super) mod counter {
pub struct Counter(AtomicU32);
impl Counter {
#[allow(dead_code)]
pub const fn new() -> Self {
Self(AtomicU32::new(0))
}
@@ -203,20 +204,20 @@ pub struct SuspenseCounters {
}
#[component]
pub fn InstrumentedRoutes() -> impl MatchNestedRoutes + Clone {
pub fn InstrumentedRoutes() -> impl leptos_router::MatchNestedRoutes + Clone {
// TODO should make this mode configurable via feature flag?
let ssr = SsrMode::Async;
view! {
<ParentRoute path=StaticSegment("instrumented") view=InstrumentedRoot ssr>
<Route path=StaticSegment("/") view=InstrumentedTop/>
<Route path=StaticSegment("/") view=InstrumentedTop />
<ParentRoute path=StaticSegment("item") view=ItemRoot>
<Route path=StaticSegment("/") view=ItemListing/>
<Route path=StaticSegment("/") view=ItemListing />
<ParentRoute path=ParamSegment("id") view=ItemTop>
<Route path=StaticSegment("/") view=ItemOverview/>
<Route path=WildcardSegment("path") view=ItemInspect/>
<Route path=StaticSegment("/") view=ItemOverview />
<Route path=WildcardSegment("path") view=ItemInspect />
</ParentRoute>
</ParentRoute>
<Route path=StaticSegment("counters") view=ShowCounters/>
<Route path=StaticSegment("counters") view=ShowCounters />
</ParentRoute>
}
.into_inner()
@@ -279,32 +280,41 @@ fn InstrumentedRoot() -> impl IntoView {
<section id="instrumented">
<nav>
<a href="/">"Site Root"</a>
<A href="./" exact=true>"Instrumented Root"</A>
<A href="item/" strict_trailing_slash=true>"Item Listing"</A>
<A href="counters" strict_trailing_slash=true>"Counters"</A>
<A href="./" exact=true>
"Instrumented Root"
</A>
<A href="item/" strict_trailing_slash=true>
"Item Listing"
</A>
<A href="counters" strict_trailing_slash=true>
"Counters"
</A>
</nav>
<FieldNavPortlet/>
<Outlet/>
<Suspense>{
move || Suspend::new(async move {
<FieldNavPortlet />
<Outlet />
<Suspense>
{move || Suspend::new(async move {
let clear_suspense_counters = move |_| {
counters.update(|c| *c = SuspenseCounters::default());
};
csr_ticket.get().map(|ticket| {
let ticket = ticket.0;
view! {
<ActionForm action=reset_counters>
<input type="hidden" name="ticket" value=format!("{ticket}") />
<input
id="reset-csr-counters"
type="submit"
value="Reset CSR Counters"
on:click=clear_suspense_counters/>
</ActionForm>
}
})
})
}</Suspense>
csr_ticket
.get()
.map(|ticket| {
let ticket = ticket.0;
view! {
<ActionForm action=reset_counters>
<input type="hidden" name="ticket" value=format!("{ticket}") />
<input
id="reset-csr-counters"
type="submit"
value="Reset CSR Counters"
on:click=clear_suspense_counters
/>
</ActionForm>
}
})
})}
</Suspense>
<footer>
<nav>
<A href="item/3/">"Target 3##"</A>
@@ -323,11 +333,17 @@ fn InstrumentedRoot() -> impl IntoView {
fn InstrumentedTop() -> impl IntoView {
view! {
<h1>"Instrumented Tests"</h1>
<p>"These tests validates the number of invocations of server functions and suspenses per access."</p>
<p>
"These tests validates the number of invocations of server functions and suspenses per access."
</p>
<ul>
// not using `A` because currently some bugs with artix
<li><a href="item/">"Item Listing"</a></li>
<li><a href="item/4/path1/">"Target 41#"</a></li>
<li>
<a href="item/">"Item Listing"</a>
</li>
<li>
<a href="item/4/path1/">"Target 41#"</a>
</li>
</ul>
}
}
@@ -342,7 +358,7 @@ fn ItemRoot() -> impl IntoView {
view! {
<h2>"<ItemRoot/>"</h2>
<Outlet/>
<Outlet />
}
}
@@ -360,7 +376,9 @@ fn ItemListing() -> impl IntoView {
// adding an extra `/` in artix; manually construct `a` instead.
// <li><A href=format!("./{item}/")>"Item "{item}</A></li>
view! {
<li><a href=format!("/instrumented/item/{item}/")>"Item "{item}</a></li>
<li>
<a href=format!("/instrumented/item/{item}/")>"Item "{item}</a>
</li>
}
)
.collect_view()
@@ -373,9 +391,7 @@ fn ItemListing() -> impl IntoView {
view! {
<h3>"<ItemListing/>"</h3>
<ul>
<Suspense>
{item_listing}
</Suspense>
<Suspense>{item_listing}</Suspense>
</ul>
}
}
@@ -402,7 +418,7 @@ fn ItemTop() -> impl IntoView {
));
view! {
<h4>"<ItemTop/>"</h4>
<Outlet/>
<Outlet />
}
}
@@ -412,24 +428,29 @@ fn ItemOverview() -> impl IntoView {
let resource = expect_context::<Resource<Option<GetItemResult>>>();
let item_view = move || {
Suspend::new(async move {
let result = resource.await.map(|GetItemResult(item, names)| view! {
<p>{format!("Viewing {item:?}")}</p>
<ul>{
names.into_iter()
.map(|name| {
// FIXME seems like relative link isn't working, it is currently
// adding an extra `/` in artix; manually construct `a` instead.
// <li><A href=format!("./{name}/")>{format!("Inspect {name}")}</A></li>
let id = item.id;
view! {
<li><a href=format!("/instrumented/item/{id}/{name}/")>
"Inspect "{name.clone()}
</a></li>
}
})
.collect_view()
}</ul>
});
let result = resource.await.map(|GetItemResult(item, names)| {
view! {
<p>{format!("Viewing {item:?}")}</p>
<ul>
{names
.into_iter()
.map(|name| {
let id = item.id;
// FIXME seems like relative link isn't working, it is currently
// adding an extra `/` in artix; manually construct `a` instead.
// <li><A href=format!("./{name}/")>{format!("Inspect {name}")}</A></li>
view! {
<li>
<a href=format!(
"/instrumented/item/{id}/{name}/",
)>"Inspect "{name.clone()}</a>
</li>
}
})
.collect_view()}
</ul>
}
});
suspense_counters.update_untracked(|c| c.item_overview += 1);
result
})
@@ -437,9 +458,7 @@ fn ItemOverview() -> impl IntoView {
view! {
<h5>"<ItemOverview/>"</h5>
<Suspense>
{item_view}
</Suspense>
<Suspense>{item_view}</Suspense>
}
}
@@ -473,8 +492,9 @@ fn ItemInspect() -> impl IntoView {
// result
},
);
on_cleanup(|| {
if let Some(c) = use_context::<WriteSignal<Option<FieldNavCtx>>>() {
let ws = use_context::<WriteSignal<Option<FieldNavCtx>>>();
on_cleanup(move || {
if let Some(c) = ws {
c.set(None);
}
});
@@ -496,23 +516,26 @@ fn ItemInspect() -> impl IntoView {
));
view! {
<p>{format!("Inspecting {item:?}")}</p>
<ul>{
fields.iter()
<ul>
{fields
.iter()
.map(|field| {
// FIXME seems like relative link to root for a wildcard isn't
// working as expected, so manually construct `a` instead.
// let text = format!("Inspect {name}/{field}");
// view! {
// <li><A href=format!("{field}")>{text}</A></li>
// <li><A href=format!("{field}")>{text}</A></li>
// }
view! {
<li><a href=format!("/instrumented/item/{id}/{name}/{field}")>{
format!("Inspect {name}/{field}")
}</a></li>
<li>
<a href=format!(
"/instrumented/item/{id}/{name}/{field}",
)>{format!("Inspect {name}/{field}")}</a>
</li>
}
})
.collect_view()
}</ul>
.collect_view()}
</ul>
}
});
suspense_counters.update_untracked(|c| c.item_inspect += 1);
@@ -527,9 +550,7 @@ fn ItemInspect() -> impl IntoView {
view! {
<h5>"<ItemInspect/>"</h5>
<Suspense>
{inspect_view}
</Suspense>
<Suspense>{inspect_view}</Suspense>
}
}
@@ -590,7 +611,8 @@ fn ShowCounters() -> impl IntoView {
id="reset-counters"
type="submit"
value="Reset Counters"
on:click=clear_suspense_counters/>
on:click=clear_suspense_counters
/>
</ActionForm>
}
})
@@ -601,20 +623,23 @@ fn ShowCounters() -> impl IntoView {
<h2>"Counters"</h2>
<h3 id="suspend-calls">"Suspend Calls"</h3>
{move || suspense_counters.with(|c| view! {
<dl>
<dt>"item_listing"</dt>
<dd id="item_listing">{c.item_listing}</dd>
<dt>"item_overview"</dt>
<dd id="item_overview">{c.item_overview}</dd>
<dt>"item_inspect"</dt>
<dd id="item_inspect">{c.item_inspect}</dd>
</dl>
})}
{move || {
suspense_counters
.with(|c| {
view! {
<dl>
<dt>"item_listing"</dt>
<dd id="item_listing">{c.item_listing}</dd>
<dt>"item_overview"</dt>
<dd id="item_overview">{c.item_overview}</dd>
<dt>"item_inspect"</dt>
<dd id="item_inspect">{c.item_inspect}</dd>
</dl>
}
})
}}
<Suspense>
{counter_view}
</Suspense>
<Suspense>{counter_view}</Suspense>
}
}
@@ -642,17 +667,17 @@ pub fn FieldNavPortlet() -> impl IntoView {
view! {
<div id="FieldNavPortlet">
<span>"FieldNavPortlet:"</span>
<nav>{
ctx.0.map(|ctx| {
ctx.into_iter()
.map(|FieldNavItem { href, text }| {
view! {
<A href=href>{text}</A>
}
})
.collect_view()
})
}</nav>
<nav>
{ctx
.0
.map(|ctx| {
ctx.into_iter()
.map(|FieldNavItem { href, text }| {
view! { <A href=href>{text}</A> }
})
.collect_view()
})}
</nav>
</div>
}
})

View File

@@ -7,7 +7,7 @@ edition = "2021"
crate-type = ["cdylib", "rlib"]
[dependencies]
axum = { version = "0.7.5", optional = true }
axum = { version = "0.8.1", optional = true }
console_error_panic_hook = "0.1.7"
leptos = { path = "../../leptos" }
leptos_meta = { path = "../../meta" }

22
examples/tailwind_axum/package-lock.json generated Normal file
View File

@@ -0,0 +1,22 @@
{
"name": "leptos-tailwind",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "leptos-tailwind",
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
"tailwindcss": "^4.0.0"
}
},
"node_modules/tailwindcss": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.0.tgz",
"integrity": "sha512-ULRPI3A+e39T7pSaf1xoi58AqqJxVCLg8F/uM5A3FadUbnyDTgltVnXJvdkTjwCOGA6NazqHVcwPJC5h2vRYVQ==",
"dev": true
}
}
}

View File

@@ -0,0 +1,69 @@
/*! tailwindcss v4.0.0 | MIT License | https://tailwindcss.com */
@tailwind base;
@tailwind components;
.relative {
position: relative;
}
.m-auto {
margin: auto;
}
.flex {
display: flex;
}
.min-h-screen {
min-height: 100vh;
}
.transform {
transform: var(--tw-rotate-x) var(--tw-rotate-y) var(--tw-rotate-z) var(--tw-skew-x) var(--tw-skew-y);
}
.flex-col {
flex-direction: column;
}
.flex-row-reverse {
flex-direction: row-reverse;
}
.flex-wrap {
flex-wrap: wrap;
}
.border-b-4 {
border-bottom-style: var(--tw-border-style);
border-bottom-width: 4px;
}
.border-l-2 {
border-left-style: var(--tw-border-style);
border-left-width: 2px;
}
.bg-gradient-to-tl {
--tw-gradient-position: to top left in oklab,;
background-image: linear-gradient(var(--tw-gradient-stops));
}
@property --tw-rotate-x {
syntax: "*";
inherits: false;
initial-value: rotateX(0);
}
@property --tw-rotate-y {
syntax: "*";
inherits: false;
initial-value: rotateY(0);
}
@property --tw-rotate-z {
syntax: "*";
inherits: false;
initial-value: rotateZ(0);
}
@property --tw-skew-x {
syntax: "*";
inherits: false;
initial-value: skewX(0);
}
@property --tw-skew-y {
syntax: "*";
inherits: false;
initial-value: skewY(0);
}
@property --tw-border-style {
syntax: "*";
inherits: false;
initial-value: solid;
}

View File

@@ -16,7 +16,7 @@ leptos_axum = { path = "../../integrations/axum", optional = true }
log = "0.4.22"
simple_logger = "5.0"
serde = { version = "1.0", features = ["derive"] }
axum = { version = "0.7.5", optional = true }
axum = { version = "0.8.1", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
tokio = { version = "1.39", features = ["full"], optional = true }

View File

@@ -1,4 +1,4 @@
use crate::todo::*;
#[cfg(feature = "ssr")]
use axum::{
body::Body,
extract::Path,
@@ -8,10 +8,9 @@ use axum::{
Router,
};
use leptos::prelude::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use todo_app_sqlite_axum::*;
//Define a handler to test extractor with state
#[cfg(feature = "ssr")]
async fn custom_handler(
Path(id): Path<String>,
req: Request<Body>,
@@ -20,14 +19,16 @@ async fn custom_handler(
move || {
provide_context(id.clone());
},
TodoApp,
todo::TodoApp,
);
handler(req).await.into_response()
}
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use crate::todo::ssr::db;
use crate::todo::{ssr::db, *};
use leptos_axum::{generate_route_list, LeptosRoutes};
simple_logger::init_with_level(log::Level::Error)
.expect("couldn't initialize logging");
@@ -45,7 +46,7 @@ async fn main() {
// build our application with a route
let app = Router::new()
.route("/special/:id", get(custom_handler))
.route("/special/{id}", get(custom_handler))
.leptos_routes(&leptos_options, routes, {
let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone())
@@ -61,3 +62,12 @@ async fn main() {
.await
.unwrap();
}
#[cfg(not(feature = "ssr"))]
pub fn main() {
use leptos::mount::mount_to_body;
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(todo::TodoApp);
}

View File

@@ -15,7 +15,7 @@ leptos_meta = { path = "../../meta" }
leptos_router = { path = "../../router" }
leptos_integration_utils = { path = "../../integrations/utils", optional = true }
serde = { version = "1.0", features = ["derive"] }
axum = { version = "0.7.5", optional = true }
axum = { version = "0.8.1", optional = true }
tower = { version = "0.5.1", features = ["util"], optional = true }
tower-http = { version = "0.6.1", features = ["fs"], optional = true }
tokio = { version = "1.39", features = ["full"], optional = true }

View File

@@ -34,7 +34,7 @@ async fn main() {
// here, we're not actually doing server side rendering, so we set up a manual
// handler for the server fns
// this should include a get() handler if you have any GetUrl-based server fns
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
.route("/api/{*fn_name}", post(leptos_axum::handle_server_fns))
.fallback(file_or_index_handler)
.with_state(leptos_options);

View File

@@ -1,6 +1,6 @@
[package]
name = "hydration_context"
version = "0.2.1"
version = "0.3.0"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
@@ -14,7 +14,7 @@ throw_error = { workspace = true }
or_poisoned = { workspace = true }
futures = "0.3.31"
serde = { version = "1.0", features = ["derive"] }
wasm-bindgen = { version = "0.2.97", optional = true }
wasm-bindgen = { version = "0.2.100", optional = true }
js-sys = { version = "0.3.74", optional = true }
once_cell = "1.20"
pin-project-lite = "0.2.15"

View File

@@ -21,10 +21,11 @@ leptos_macro = { workspace = true, features = ["actix"] }
leptos_meta = { workspace = true, features = ["nonce"] }
leptos_router = { workspace = true, features = ["ssr"] }
server_fn = { workspace = true, features = ["actix"] }
serde_json = "1.0"
tachys = { workspace = true }
serde_json = { workspace = true }
parking_lot = "0.12.3"
tracing = { version = "0.1", optional = true }
tokio = { version = "1.41", features = ["rt", "fs"] }
tokio = { version = "1.43", features = ["rt", "fs"] }
send_wrapper = "0.6.0"
dashmap = "6"
once_cell = "1"
@@ -33,7 +34,7 @@ once_cell = "1"
rustdoc-args = ["--generate-link-to-definition"]
[features]
dont-use-islands-router = []
islands-router = ["tachys/islands"]
tracing = ["dep:tracing"]
[package.metadata.cargo-all-features]

View File

@@ -23,6 +23,7 @@ use hydration_context::SsrSharedContext;
use leptos::{
config::LeptosOptions,
context::{provide_context, use_context},
hydration::IslandsRouterNavigation,
prelude::expect_context,
reactive::{computed::ScopedFuture, owner::Owner},
IntoView,
@@ -274,14 +275,13 @@ pub fn redirect(path: &str) {
///
/// This can then be set up at an appropriate route in your application:
///
/// ```
/// ```no_run
/// use actix_web::*;
///
/// fn register_server_functions() {
/// // call ServerFn::register() for each of the server functions you've defined
/// }
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[actix_web::main]
/// async fn main() -> std::io::Result<()> {
/// // make sure you actually register your server functions
@@ -297,7 +297,6 @@ pub fn redirect(path: &str) {
/// .run()
/// .await
/// }
/// # }
/// ```
///
/// ## Provided Context Types
@@ -369,7 +368,6 @@ pub fn handle_server_fns_with_context(
// actually run the server fn
let mut res = ActixResponse(
service
.0
.run(ActixRequest::from((req, payload)))
.await
.take(),
@@ -433,7 +431,7 @@ pub fn handle_server_fns_with_context(
/// but requires some client-side JavaScript.
///
/// This can then be set up at an appropriate route in your application:
/// ```
/// ```no_run
/// use actix_web::{App, HttpServer};
/// use leptos::prelude::*;
/// use leptos_router::Method;
@@ -444,7 +442,6 @@ pub fn handle_server_fns_with_context(
/// view! { <main>"Hello, world!"</main> }
/// }
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[actix_web::main]
/// async fn main() -> std::io::Result<()> {
/// let conf = get_configuration(Some("Cargo.toml")).unwrap();
@@ -464,7 +461,6 @@ pub fn handle_server_fns_with_context(
/// .run()
/// .await
/// }
/// # }
/// ```
///
/// ## Provided Context Types
@@ -492,7 +488,7 @@ where
/// sending down its HTML. The app will become interactive once it has fully loaded.
///
/// This can then be set up at an appropriate route in your application:
/// ```
/// ```no_run
/// use actix_web::{App, HttpServer};
/// use leptos::prelude::*;
/// use leptos_router::Method;
@@ -503,7 +499,6 @@ where
/// view! { <main>"Hello, world!"</main> }
/// }
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[actix_web::main]
/// async fn main() -> std::io::Result<()> {
/// let conf = get_configuration(Some("Cargo.toml")).unwrap();
@@ -526,7 +521,6 @@ where
/// .run()
/// .await
/// }
/// # }
/// ```
///
/// ## Provided Context Types
@@ -552,7 +546,7 @@ where
/// `async` resources have loaded.
///
/// This can then be set up at an appropriate route in your application:
/// ```
/// ```no_run
/// use actix_web::{App, HttpServer};
/// use leptos::prelude::*;
/// use leptos_router::Method;
@@ -563,7 +557,6 @@ where
/// view! { <main>"Hello, world!"</main> }
/// }
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[actix_web::main]
/// async fn main() -> std::io::Result<()> {
/// let conf = get_configuration(Some("Cargo.toml")).unwrap();
@@ -583,7 +576,6 @@ where
/// .run()
/// .await
/// }
/// # }
/// ```
///
/// ## Provided Context Types
@@ -663,12 +655,27 @@ where
IV: IntoView + 'static,
{
_ = replace_blocks; // TODO
handle_response(method, additional_context, app_fn, |app, chunks| {
Box::pin(async move {
Box::pin(app.to_html_stream_out_of_order().chain(chunks()))
as PinnedStream<String>
})
})
handle_response(
method,
additional_context,
app_fn,
|app, chunks, supports_ooo| {
Box::pin(async move {
let app = if cfg!(feature = "islands-router") {
if supports_ooo {
app.to_html_stream_out_of_order_branching()
} else {
app.to_html_stream_in_order_branching()
}
} else if supports_ooo {
app.to_html_stream_out_of_order()
} else {
app.to_html_stream_in_order()
};
Box::pin(app.chain(chunks())) as PinnedStream<String>
})
},
)
}
/// Returns an Actix [struct@Route](actix_web::Route) that listens for a `GET` request and tries
@@ -694,12 +701,21 @@ pub fn render_app_to_stream_in_order_with_context<IV>(
where
IV: IntoView + 'static,
{
handle_response(method, additional_context, app_fn, |app, chunks| {
Box::pin(async move {
Box::pin(app.to_html_stream_in_order().chain(chunks()))
as PinnedStream<String>
})
})
handle_response(
method,
additional_context,
app_fn,
|app, chunks, _supports_ooo| {
Box::pin(async move {
let app = if cfg!(feature = "islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
};
Box::pin(app.chain(chunks())) as PinnedStream<String>
})
},
)
}
/// Returns an Actix [struct@Route](actix_web::Route) that listens for a `GET` request and tries
@@ -731,12 +747,13 @@ where
fn async_stream_builder<IV>(
app: IV,
chunks: BoxedFnOnce<PinnedStream<String>>,
_supports_ooo: bool,
) -> PinnedFuture<PinnedStream<String>>
where
IV: IntoView + 'static,
{
Box::pin(async move {
let app = if cfg!(feature = "dont-use-islands-router") {
let app = if cfg!(feature = "islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
@@ -776,6 +793,7 @@ fn leptos_corrected_path(req: &HttpRequest) -> String {
}
}
#[allow(clippy::type_complexity)]
fn handle_response<IV>(
method: Method,
additional_context: impl Fn() + 'static + Clone + Send,
@@ -783,6 +801,7 @@ fn handle_response<IV>(
stream_builder: fn(
IV,
BoxedFnOnce<PinnedStream<String>>,
bool,
) -> PinnedFuture<PinnedStream<String>>,
) -> Route
where
@@ -793,6 +812,9 @@ where
let add_context = additional_context.clone();
async move {
let is_island_router_navigation = cfg!(feature = "islands-router")
&& req.headers().get("Islands-Router").is_some();
let res_options = ResponseOptions::default();
let (meta_context, meta_output) = ServerMetaContext::new();
@@ -803,6 +825,10 @@ where
move || {
provide_contexts(req, &meta_context, &res_options);
add_context();
if is_island_router_navigation {
provide_context(IslandsRouterNavigation);
}
}
};
@@ -812,6 +838,7 @@ where
additional_context,
res_options,
stream_builder,
!is_island_router_navigation,
)
.await;
@@ -1102,6 +1129,7 @@ impl StaticRouteGenerator {
app_fn.clone(),
additional_context,
async_stream_builder,
false,
);
let sc = owner.shared_context().unwrap();

View File

@@ -4,14 +4,14 @@ authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
description = "Axum integrations for the Leptos web framework."
version = { workspace = true }
version = "0.8.0-beta"
rust-version.workspace = true
edition.workspace = true
[dependencies]
any_spawner = { workspace = true, features = ["tokio"] }
hydration_context = { workspace = true }
axum = { version = "0.7.9", default-features = false, features = [
axum = { version = "0.8.1", default-features = false, features = [
"matched-path",
] }
dashmap = "6"
@@ -22,21 +22,28 @@ leptos_macro = { workspace = true, features = ["axum"] }
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.41", default-features = false }
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 }
[dev-dependencies]
axum = "0.7.9"
tokio = { version = "1.41", features = ["net", "rt-multi-thread"] }
axum = "0.8.1"
tokio = { version = "1.43", features = ["net", "rt-multi-thread"] }
[features]
wasm = []
default = ["tokio/fs", "tokio/sync", "tower-http/fs", "tower/util"]
dont-use-islands-router = []
default = [
"tokio/fs",
"tokio/sync",
"tower-http/fs",
"tower/util",
"server_fn/axum",
]
islands-router = ["tachys/islands"]
tracing = ["dep:tracing"]
[package.metadata.docs.rs]

View File

@@ -1,5 +1,6 @@
#![forbid(unsafe_code)]
#![deny(missing_docs)]
#![allow(clippy::type_complexity)]
//! Provides functions to easily integrate Leptos with Axum.
//!
@@ -278,12 +279,11 @@ pub fn generate_request_and_parts(
///
/// This can then be set up at an appropriate route in your application:
///
/// ```
/// ```no_run
/// use axum::{handler::Handler, routing::post, Router};
/// use leptos::prelude::*;
/// use std::net::SocketAddr;
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[cfg(feature = "default")]
/// #[tokio::main]
/// async fn main() {
@@ -299,7 +299,9 @@ pub fn generate_request_and_parts(
/// .await
/// .unwrap();
/// }
/// # }
///
/// # #[cfg(not(feature = "default"))]
/// # fn main() { }
/// ```
/// Leptos provides a generic implementation of `handle_server_fns`. If access to more specific parts of the Request is desired,
/// you can specify your own server fn handler based on this one and give it it's own route in the server macro.
@@ -368,8 +370,6 @@ async fn handle_server_fns_inner(
additional_context: impl Fn() + 'static + Clone + Send,
req: Request<Body>,
) -> impl IntoResponse {
use server_fn::middleware::Service;
let method = req.method().clone();
let path = req.uri().path().to_string();
let (req, parts) = generate_request_and_parts(req);
@@ -442,7 +442,7 @@ pub type PinnedHtmlStream =
/// to route it using [leptos_router], serving an HTML stream of your application.
///
/// This can then be set up at an appropriate route in your application:
/// ```
/// ```no_run
/// use axum::{handler::Handler, Router};
/// use leptos::{config::get_configuration, prelude::*};
/// use std::{env, net::SocketAddr};
@@ -452,7 +452,6 @@ pub type PinnedHtmlStream =
/// view! { <main>"Hello, world!"</main> }
/// }
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[cfg(feature = "default")]
/// #[tokio::main]
/// async fn main() {
@@ -471,7 +470,9 @@ pub type PinnedHtmlStream =
/// .await
/// .unwrap();
/// }
/// # }
///
/// # #[cfg(not(feature = "default"))]
/// # fn main() { }
/// ```
///
/// ## Provided Context Types
@@ -484,7 +485,7 @@ pub type PinnedHtmlStream =
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream<IV>(
app_fn: impl Fn() -> IV + Clone + Send + 'static,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
@@ -508,7 +509,7 @@ where
)]
pub fn render_route<S, IV>(
paths: Vec<AxumRouteListing>,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
) -> impl Fn(
State<S>,
Request<Body>,
@@ -530,7 +531,7 @@ where
/// sending down its HTML. The app will become interactive once it has fully loaded.
///
/// This can then be set up at an appropriate route in your application:
/// ```
/// ```no_run
/// use axum::{handler::Handler, Router};
/// use leptos::{config::get_configuration, prelude::*};
/// use std::{env, net::SocketAddr};
@@ -540,7 +541,6 @@ where
/// view! { <main>"Hello, world!"</main> }
/// }
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[cfg(feature = "default")]
/// #[tokio::main]
/// async fn main() {
@@ -559,7 +559,9 @@ where
/// .await
/// .unwrap();
/// }
/// # }
///
/// # #[cfg(not(feature = "default"))]
/// # fn main() { }
/// ```
///
/// ## Provided Context Types
@@ -572,7 +574,7 @@ where
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream_in_order<IV>(
app_fn: impl Fn() -> IV + Clone + Send + 'static,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
@@ -625,13 +627,14 @@ where
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream_with_context<IV>(
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
+ Clone
+ Send
+ Sync
+ 'static
where
IV: IntoView + 'static,
@@ -654,8 +657,8 @@ where
)]
pub fn render_route_with_context<S, IV>(
paths: Vec<AxumRouteListing>,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
) -> impl Fn(
State<S>,
Request<Body>,
@@ -756,25 +759,32 @@ where
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream_with_context_and_replace_blocks<IV>(
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
replace_blocks: bool,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
+ Clone
+ Send
+ Sync
+ 'static
where
IV: IntoView + 'static,
{
_ = replace_blocks; // TODO
handle_response(additional_context, app_fn, |app, chunks| {
handle_response(additional_context, app_fn, |app, chunks, supports_ooo| {
Box::pin(async move {
let app = if cfg!(feature = "dont-use-islands-router") {
app.to_html_stream_out_of_order_branching()
} else {
let app = if cfg!(feature = "islands-router") {
if supports_ooo {
app.to_html_stream_out_of_order_branching()
} else {
app.to_html_stream_in_order_branching()
}
} else if supports_ooo {
app.to_html_stream_out_of_order()
} else {
app.to_html_stream_in_order()
};
Box::pin(app.chain(chunks())) as PinnedStream<String>
})
@@ -823,8 +833,8 @@ where
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream_in_order_with_context<IV>(
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
@@ -834,8 +844,8 @@ pub fn render_app_to_stream_in_order_with_context<IV>(
where
IV: IntoView + 'static,
{
handle_response(additional_context, app_fn, |app, chunks| {
let app = if cfg!(feature = "dont-use-islands-router") {
handle_response(additional_context, app_fn, |app, chunks, _supports_ooo| {
let app = if cfg!(feature = "islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
@@ -847,13 +857,18 @@ where
}
fn handle_response<IV>(
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
stream_builder: fn(
IV,
BoxedFnOnce<PinnedStream<String>>,
bool,
) -> PinnedFuture<PinnedStream<String>>,
) -> impl Fn(Request<Body>) -> PinnedFuture<Response<Body>> + Clone + Send + 'static
) -> impl Fn(Request<Body>) -> PinnedFuture<Response<Body>>
+ Clone
+ Send
+ Sync
+ 'static
where
IV: IntoView + 'static,
{
@@ -871,12 +886,16 @@ fn handle_response_inner<IV>(
stream_builder: fn(
IV,
BoxedFnOnce<PinnedStream<String>>,
bool,
) -> PinnedFuture<PinnedStream<String>>,
) -> PinnedFuture<Response<Body>>
where
IV: IntoView + 'static,
{
Box::pin(async move {
let is_island_router_navigation = cfg!(feature = "islands-router")
&& req.headers().get("Islands-Router").is_some();
let add_context = additional_context.clone();
let res_options = ResponseOptions::default();
let (meta_context, meta_output) = ServerMetaContext::new();
@@ -898,6 +917,10 @@ where
res_options.clone(),
);
add_context();
if is_island_router_navigation {
provide_context(IslandsRouterNavigation);
}
}
};
@@ -907,6 +930,7 @@ where
additional_context,
res_options,
stream_builder,
!is_island_router_navigation,
)
.await;
@@ -937,7 +961,7 @@ fn provide_contexts(
/// `async` resources have loaded.
///
/// This can then be set up at an appropriate route in your application:
/// ```
/// ```no_run
/// use axum::{handler::Handler, Router};
/// use leptos::{config::get_configuration, prelude::*};
/// use std::{env, net::SocketAddr};
@@ -947,7 +971,6 @@ fn provide_contexts(
/// view! { <main>"Hello, world!"</main> }
/// }
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[cfg(feature = "default")]
/// #[tokio::main]
/// async fn main() {
@@ -967,7 +990,9 @@ fn provide_contexts(
/// .await
/// .unwrap();
/// }
/// # }
///
/// # #[cfg(not(feature = "default"))]
/// # fn main() { }
/// ```
///
/// ## Provided Context Types
@@ -980,7 +1005,7 @@ fn provide_contexts(
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_async<IV>(
app_fn: impl Fn() -> IV + Clone + Send + 'static,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
@@ -1034,8 +1059,8 @@ where
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_async_stream_with_context<IV>(
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
@@ -1045,9 +1070,9 @@ pub fn render_app_async_stream_with_context<IV>(
where
IV: IntoView + 'static,
{
handle_response(additional_context, app_fn, |app, chunks| {
handle_response(additional_context, app_fn, |app, chunks, _supports_ooo| {
Box::pin(async move {
let app = if cfg!(feature = "dont-use-islands-router") {
let app = if cfg!(feature = "islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
@@ -1101,8 +1126,8 @@ where
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_async_with_context<IV>(
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
@@ -1118,12 +1143,13 @@ where
fn async_stream_builder<IV>(
app: IV,
chunks: BoxedFnOnce<PinnedStream<String>>,
_supports_ooo: bool,
) -> PinnedFuture<PinnedStream<String>>
where
IV: IntoView + 'static,
{
Box::pin(async move {
let app = if cfg!(feature = "dont-use-islands-router") {
let app = if cfg!(feature = "islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
@@ -1396,6 +1422,7 @@ impl StaticRouteGenerator {
app_fn.clone(),
additional_context,
async_stream_builder,
false,
);
let sc = owner.shared_context().unwrap();
@@ -1641,7 +1668,7 @@ where
self,
options: &S,
paths: Vec<AxumRouteListing>,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
) -> Self
where
IV: IntoView + 'static;
@@ -1656,8 +1683,8 @@ where
self,
options: &S,
paths: Vec<AxumRouteListing>,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
) -> Self
where
IV: IntoView + 'static;
@@ -1690,12 +1717,15 @@ impl AxumPath for Vec<PathSegment> {
match segment {
PathSegment::Static(s) => path.push_str(s),
PathSegment::Param(s) => {
path.push(':');
path.push('{');
path.push_str(s);
path.push('}');
}
PathSegment::Splat(s) => {
path.push('{');
path.push('*');
path.push_str(s);
path.push('}');
}
PathSegment::Unit => {}
PathSegment::OptionalParam(_) => {
@@ -1727,7 +1757,7 @@ where
self,
state: &S,
paths: Vec<AxumRouteListing>,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
) -> Self
where
IV: IntoView + 'static,
@@ -1743,8 +1773,8 @@ where
self,
state: &S,
paths: Vec<AxumRouteListing>,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
) -> Self
where
IV: IntoView + 'static,
@@ -1825,64 +1855,64 @@ where
}
} else {
router.route(
path,
match listing.mode() {
SsrMode::OutOfOrder => {
let s = render_app_to_stream_with_context(
cx_with_state_and_method.clone(),
app_fn.clone(),
);
match method {
leptos_router::Method::Get => get(s),
leptos_router::Method::Post => post(s),
leptos_router::Method::Put => put(s),
leptos_router::Method::Delete => delete(s),
leptos_router::Method::Patch => patch(s),
path,
match listing.mode() {
SsrMode::OutOfOrder => {
let s = render_app_to_stream_with_context(
cx_with_state_and_method.clone(),
app_fn.clone(),
);
match method {
leptos_router::Method::Get => get(s),
leptos_router::Method::Post => post(s),
leptos_router::Method::Put => put(s),
leptos_router::Method::Delete => delete(s),
leptos_router::Method::Patch => patch(s),
}
}
}
SsrMode::PartiallyBlocked => {
let s = render_app_to_stream_with_context_and_replace_blocks(
cx_with_state_and_method.clone(),
app_fn.clone(),
true
);
match method {
leptos_router::Method::Get => get(s),
leptos_router::Method::Post => post(s),
leptos_router::Method::Put => put(s),
leptos_router::Method::Delete => delete(s),
leptos_router::Method::Patch => patch(s),
SsrMode::PartiallyBlocked => {
let s = render_app_to_stream_with_context_and_replace_blocks(
cx_with_state_and_method.clone(),
app_fn.clone(),
true
);
match method {
leptos_router::Method::Get => get(s),
leptos_router::Method::Post => post(s),
leptos_router::Method::Put => put(s),
leptos_router::Method::Delete => delete(s),
leptos_router::Method::Patch => patch(s),
}
}
}
SsrMode::InOrder => {
let s = render_app_to_stream_in_order_with_context(
cx_with_state_and_method.clone(),
app_fn.clone(),
);
match method {
leptos_router::Method::Get => get(s),
leptos_router::Method::Post => post(s),
leptos_router::Method::Put => put(s),
leptos_router::Method::Delete => delete(s),
leptos_router::Method::Patch => patch(s),
SsrMode::InOrder => {
let s = render_app_to_stream_in_order_with_context(
cx_with_state_and_method.clone(),
app_fn.clone(),
);
match method {
leptos_router::Method::Get => get(s),
leptos_router::Method::Post => post(s),
leptos_router::Method::Put => put(s),
leptos_router::Method::Delete => delete(s),
leptos_router::Method::Patch => patch(s),
}
}
}
SsrMode::Async => {
let s = render_app_async_with_context(
cx_with_state_and_method.clone(),
app_fn.clone(),
);
match method {
leptos_router::Method::Get => get(s),
leptos_router::Method::Post => post(s),
leptos_router::Method::Put => put(s),
leptos_router::Method::Delete => delete(s),
leptos_router::Method::Patch => patch(s),
SsrMode::Async => {
let s = render_app_async_with_context(
cx_with_state_and_method.clone(),
app_fn.clone(),
);
match method {
leptos_router::Method::Get => get(s),
leptos_router::Method::Post => post(s),
leptos_router::Method::Put => put(s),
leptos_router::Method::Delete => delete(s),
leptos_router::Method::Patch => patch(s),
}
}
}
_ => unreachable!()
},
)
_ => unreachable!()
},
)
};
}
}
@@ -1983,7 +2013,7 @@ where
#[cfg(feature = "default")]
pub fn file_and_error_handler_with_context<S, IV>(
additional_context: impl Fn() + 'static + Clone + Send,
shell: fn(LeptosOptions) -> IV,
shell: impl Fn(LeptosOptions) -> IV + 'static + Clone + Send,
) -> impl Fn(
Uri,
State<S>,
@@ -2000,6 +2030,7 @@ where
move |uri: Uri, State(state): State<S>, req: Request<Body>| {
Box::pin({
let additional_context = additional_context.clone();
let shell = shell.clone();
async move {
let options = LeptosOptions::from_ref(&state);
let res =
@@ -2016,7 +2047,7 @@ where
},
move || shell(options),
req,
|app, chunks| {
|app, chunks, _supports_ooo| {
Box::pin(async move {
let app = app
.to_html_stream_in_order()
@@ -2043,7 +2074,7 @@ where
/// simply reuse the source code of this function in your own application.
#[cfg(feature = "default")]
pub fn file_and_error_handler<S, IV>(
shell: fn(LeptosOptions) -> IV,
shell: impl Fn(LeptosOptions) -> IV + 'static + Clone + Send,
) -> impl Fn(
Uri,
State<S>,

View File

@@ -1,3 +1,5 @@
#![allow(clippy::type_complexity)]
use futures::{stream::once, Stream, StreamExt};
use hydration_context::{SharedContext, SsrSharedContext};
use leptos::{
@@ -31,14 +33,20 @@ pub trait ExtendResponse: Sized {
stream_builder: fn(
IV,
BoxedFnOnce<PinnedStream<String>>,
bool,
) -> PinnedFuture<PinnedStream<String>>,
supports_ooo: bool,
) -> impl Future<Output = Self> + Send
where
IV: IntoView + 'static,
{
async move {
let (owner, stream) =
build_response(app_fn, additional_context, stream_builder);
let (owner, stream) = build_response(
app_fn,
additional_context,
stream_builder,
supports_ooo,
);
let sc = owner.shared_context().unwrap();
@@ -94,7 +102,11 @@ pub fn build_response<IV>(
stream_builder: fn(
IV,
BoxedFnOnce<PinnedStream<String>>,
// this argument indicates whether a request wants to support out-of-order streaming
// responses
bool,
) -> PinnedFuture<PinnedStream<String>>,
is_islands_router_navigation: bool,
) -> (Owner, PinnedFuture<PinnedStream<String>>)
where
IV: IntoView + 'static,
@@ -138,7 +150,7 @@ where
//
// we also don't actually start hydrating until after the whole stream is complete,
// so it's not useful to send those scripts down earlier.
stream_builder(app, chunks)
stream_builder(app, chunks, is_islands_router_navigation)
});
stream.await

View File

@@ -42,22 +42,17 @@ typed-builder = "0.20.0"
typed-builder-macro = "0.20.0"
serde = "1.0"
serde_json = { version = "1.0", optional = true }
server_fn = { workspace = true, features = [
"form-redirects",
"browser",
"url",
] }
server_fn = { workspace = true, features = ["form-redirects", "browser"] }
web-sys = { version = "0.3.72", features = [
"ShadowRoot",
"ShadowRootInit",
"ShadowRootMode",
] }
wasm-bindgen = "0.2.97"
wasm-bindgen = { workspace = true }
serde_qs = "0.13.0"
slotmap = "1.0"
futures = "0.3.31"
send_wrapper = "0.6.0"
getrandom = { version = "0.2", features = ["js"], optional = true }
[features]
hydration = [
@@ -66,13 +61,12 @@ hydration = [
"hydration_context/browser",
"leptos_dom/hydration",
]
csr = ["leptos_macro/csr", "reactive_graph/effects", "dep:getrandom"]
csr = ["leptos_macro/csr", "reactive_graph/effects"]
hydrate = [
"leptos_macro/hydrate",
"hydration",
"tachys/hydrate",
"reactive_graph/effects",
"dep:getrandom",
]
default-tls = ["server_fn/default-tls"]
rustls = ["server_fn/rustls"]
@@ -84,10 +78,7 @@ ssr = [
"tachys/ssr",
]
nightly = ["leptos_macro/nightly", "reactive_graph/nightly", "tachys/nightly"]
rkyv = [
"server_fn/rkyv",
"leptos_server/rkyv"
]
rkyv = ["server_fn/rkyv", "leptos_server/rkyv"]
tracing = [
"dep:tracing",
"reactive_graph/tracing",
@@ -105,6 +96,15 @@ trace-component-props = [
]
delegation = ["tachys/delegation"]
# 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
# TLDR proc macros will ignore RUSTFLAGS when --target is specified on the cargo command.
# This works around the issue by the non proc-macro crate which does see RUSTFLAGS enabling the replacement feature on the proc-macro crate, which wouldn't.
# This is automatic as long as the leptos crate is depended upon,
# downstream usage should never manually enable this feature.
[target.'cfg(erase_components)'.dependencies]
leptos_macro = { workspace = true, features = ["__internal_erase_components"] }
[package.metadata.cargo-all-features]
denylist = [
"nightly",

View File

@@ -43,7 +43,7 @@ pub fn AttributeInterceptor<Chil, T>(
) -> impl IntoView
where
Chil: Fn(AnyAttribute) -> T + Send + Sync + 'static,
T: IntoView,
T: IntoView + 'static,
{
AttributeInterceptorInner::new(children)
}
@@ -86,7 +86,7 @@ impl<T: IntoView, A: Attribute> Render for AttributeInterceptorInner<T, A> {
}
}
impl<T: IntoView, A> AddAnyAttr for AttributeInterceptorInner<T, A>
impl<T: IntoView + 'static, A> AddAnyAttr for AttributeInterceptorInner<T, A>
where
A: Attribute,
{
@@ -114,8 +114,11 @@ where
}
}
impl<T: IntoView, A: Attribute> RenderHtml for AttributeInterceptorInner<T, A> {
impl<T: IntoView + 'static, A: Attribute> RenderHtml
for AttributeInterceptorInner<T, A>
{
type AsyncOutput = T::AsyncOutput;
type Owned = AttributeInterceptorInner<T, A::CloneableOwned>;
const MIN_LENGTH: usize = T::MIN_LENGTH;
@@ -135,9 +138,15 @@ impl<T: IntoView, A: Attribute> RenderHtml for AttributeInterceptorInner<T, A> {
position: &mut leptos::tachys::view::Position,
escape: bool,
mark_branches: bool,
_extra_attrs: Vec<AnyAttribute>,
) {
self.children
.to_html_with_buf(buf, position, escape, mark_branches)
self.children.to_html_with_buf(
buf,
position,
escape,
mark_branches,
vec![],
)
}
fn hydrate<const FROM_SERVER: bool>(
@@ -147,4 +156,12 @@ impl<T: IntoView, A: Attribute> RenderHtml for AttributeInterceptorInner<T, A> {
) -> Self::State {
self.children.hydrate::<FROM_SERVER>(cursor, position)
}
fn into_owned(self) -> Self::Owned {
AttributeInterceptorInner {
children_builder: self.children_builder,
children: self.children,
attributes: self.attributes.into_cloneable_owned(),
}
}
}

View File

@@ -43,13 +43,20 @@
use reactive_graph::{
owner::{LocalStorage, StoredValue},
traits::WithValue,
traits::{Dispose, WithValue},
};
use std::{fmt, rc::Rc, sync::Arc};
/// A wrapper trait for calling callbacks.
pub trait Callable<In: 'static, Out: 'static = ()> {
/// calls the callback with the specified argument.
///
/// Returns None if the callback has been disposed
fn try_run(&self, input: In) -> Option<Out>;
/// calls the callback with the specified argument.
///
/// # Panics
/// Panics if you try to run a callback that has been disposed
fn run(&self, input: In) -> Out;
}
@@ -72,6 +79,12 @@ impl<In, Out> Clone for UnsyncCallback<In, Out> {
}
}
impl<In, Out> Dispose for UnsyncCallback<In, Out> {
fn dispose(self) {
self.0.dispose();
}
}
impl<In, Out> UnsyncCallback<In, Out> {
/// Creates a new callback from the given function.
pub fn new<F>(f: F) -> UnsyncCallback<In, Out>
@@ -93,6 +106,10 @@ impl<In, Out> UnsyncCallback<In, Out> {
}
impl<In: 'static, Out: 'static> Callable<In, Out> for UnsyncCallback<In, Out> {
fn try_run(&self, input: In) -> Option<Out> {
self.0.try_with_value(|fun| fun(input))
}
fn run(&self, input: In) -> Out {
self.0.with_value(|fun| fun(input))
}
@@ -168,10 +185,12 @@ impl<In, Out> fmt::Debug for Callback<In, Out> {
}
impl<In, Out> Callable<In, Out> for Callback<In, Out> {
fn try_run(&self, input: In) -> Option<Out> {
self.0.try_with_value(|fun| fun(input))
}
fn run(&self, input: In) -> Out {
self.0
.try_with_value(|f| f(input))
.expect("called a callback that has been disposed")
self.0.with_value(|f| f(input))
}
}
@@ -181,6 +200,12 @@ impl<In, Out> Clone for Callback<In, Out> {
}
}
impl<In, Out> Dispose for Callback<In, Out> {
fn dispose(self) {
self.0.dispose();
}
}
impl<In, Out> Copy for Callback<In, Out> {}
macro_rules! impl_callable_from_fn {
@@ -239,7 +264,9 @@ impl<In: 'static, Out: 'static> Callback<In, Out> {
#[cfg(test)]
mod tests {
use super::Callable;
use crate::callback::{Callback, UnsyncCallback};
use reactive_graph::traits::Dispose;
struct NoClone {}
@@ -270,10 +297,26 @@ mod tests {
(|num, s| format!("{num} {s}")).into();
}
#[test]
fn sync_callback_try_run() {
let callback = Callback::new(move |arg| arg);
assert_eq!(callback.try_run((0,)), Some((0,)));
callback.dispose();
assert_eq!(callback.try_run((0,)), None);
}
#[test]
fn unsync_callback_try_run() {
let callback = UnsyncCallback::new(move |arg| arg);
assert_eq!(callback.try_run((0,)), Some((0,)));
callback.dispose();
assert_eq!(callback.try_run((0,)), None);
}
#[test]
fn callback_matches_same() {
let callback1 = Callback::new(|x: i32| x * 2);
let callback2 = callback1.clone();
let callback2 = callback1;
assert!(callback1.matches(&callback2));
}
@@ -287,7 +330,7 @@ mod tests {
#[test]
fn unsync_callback_matches_same() {
let callback1 = UnsyncCallback::new(|x: i32| x * 2);
let callback2 = callback1.clone();
let callback2 = callback1;
assert!(callback1.matches(&callback2));
}

View File

@@ -91,6 +91,9 @@ pub trait ToChildren<F> {
fn to_children(f: F) -> Self;
}
/// Compiler optimisation, can be used with certain type to avoid unique closures in the view!{} macro.
pub struct ChildrenOptContainer<T>(pub T);
impl<F, C> ToChildren<F> for Children
where
F: FnOnce() -> C + Send + 'static,
@@ -102,6 +105,16 @@ where
}
}
impl<T> ToChildren<ChildrenOptContainer<T>> for Children
where
T: IntoAny + Send + 'static,
{
#[inline]
fn to_children(t: ChildrenOptContainer<T>) -> Self {
Box::new(move || t.0.into_any())
}
}
impl<F, C> ToChildren<F> for ChildrenFn
where
F: Fn() -> C + Send + Sync + 'static,
@@ -113,6 +126,16 @@ where
}
}
impl<T> ToChildren<ChildrenOptContainer<T>> for ChildrenFn
where
T: IntoAny + Clone + Send + Sync + 'static,
{
#[inline]
fn to_children(t: ChildrenOptContainer<T>) -> Self {
Arc::new(move || t.0.clone().into_any())
}
}
impl<F, C> ToChildren<F> for ChildrenFnMut
where
F: Fn() -> C + Send + 'static,
@@ -124,6 +147,16 @@ where
}
}
impl<T> ToChildren<ChildrenOptContainer<T>> for ChildrenFnMut
where
T: IntoAny + Clone + Send + 'static,
{
#[inline]
fn to_children(t: ChildrenOptContainer<T>) -> Self {
Box::new(move || t.0.clone().into_any())
}
}
impl<F, C> ToChildren<F> for BoxedChildrenFn
where
F: Fn() -> C + Send + 'static,
@@ -135,6 +168,16 @@ where
}
}
impl<T> ToChildren<ChildrenOptContainer<T>> for BoxedChildrenFn
where
T: IntoAny + Clone + Send + 'static,
{
#[inline]
fn to_children(t: ChildrenOptContainer<T>) -> Self {
Box::new(move || t.0.clone().into_any())
}
}
impl<F, C> ToChildren<F> for ChildrenFragment
where
F: FnOnce() -> C + Send + 'static,
@@ -146,6 +189,16 @@ where
}
}
impl<T> ToChildren<ChildrenOptContainer<T>> for ChildrenFragment
where
T: IntoAny + Send + 'static,
{
#[inline]
fn to_children(t: ChildrenOptContainer<T>) -> Self {
Box::new(move || Fragment::new(vec![t.0.into_any()]))
}
}
impl<F, C> ToChildren<F> for ChildrenFragmentFn
where
F: Fn() -> C + Send + 'static,
@@ -157,6 +210,16 @@ where
}
}
impl<T> ToChildren<ChildrenOptContainer<T>> for ChildrenFragmentFn
where
T: IntoAny + Clone + Send + 'static,
{
#[inline]
fn to_children(t: ChildrenOptContainer<T>) -> Self {
Arc::new(move || Fragment::new(vec![t.0.clone().into_any()]))
}
}
impl<F, C> ToChildren<F> for ChildrenFragmentMut
where
F: FnMut() -> C + Send + 'static,
@@ -168,6 +231,16 @@ where
}
}
impl<T> ToChildren<ChildrenOptContainer<T>> for ChildrenFragmentMut
where
T: IntoAny + Clone + Send + 'static,
{
#[inline]
fn to_children(t: ChildrenOptContainer<T>) -> Self {
Box::new(move || Fragment::new(vec![t.0.clone().into_any()]))
}
}
/// New-type wrapper for a function that returns a view with `From` and `Default` traits implemented
/// to enable optional props in for example `<Show>` and `<Suspense>`.
#[derive(Clone)]
@@ -246,6 +319,16 @@ where
}
}
impl<T> ToChildren<ChildrenOptContainer<T>> for TypedChildren<T>
where
T: IntoView + 'static,
{
#[inline]
fn to_children(t: ChildrenOptContainer<T>) -> Self {
TypedChildren(Box::new(move || t.0.into_view()))
}
}
/// A typed equivalent to [`ChildrenFnMut`], which takes a generic but preserves type information to
/// allow the compiler to optimize the view more effectively.
pub struct TypedChildrenMut<T>(Box<dyn FnMut() -> View<T> + Send>);
@@ -275,6 +358,16 @@ where
}
}
impl<T> ToChildren<ChildrenOptContainer<T>> for TypedChildrenMut<T>
where
T: IntoView + Clone + 'static,
{
#[inline]
fn to_children(t: ChildrenOptContainer<T>) -> Self {
TypedChildrenMut(Box::new(move || t.0.clone().into_view()))
}
}
/// A typed equivalent to [`ChildrenFn`], which takes a generic but preserves type information to
/// allow the compiler to optimize the view more effectively.
pub struct TypedChildrenFn<T>(Arc<dyn Fn() -> View<T> + Send + Sync>);
@@ -310,3 +403,13 @@ where
TypedChildrenFn(Arc::new(move || f().into_view()))
}
}
impl<T> ToChildren<ChildrenOptContainer<T>> for TypedChildrenFn<T>
where
T: IntoView + Clone + Sync + 'static,
{
#[inline]
fn to_children(t: ChildrenOptContainer<T>) -> Self {
TypedChildrenFn(Arc::new(move || t.0.clone().into_view()))
}
}

View File

@@ -11,7 +11,7 @@ use reactive_graph::{
use rustc_hash::FxHashMap;
use std::{fmt::Debug, sync::Arc};
use tachys::{
html::attribute::Attribute,
html::attribute::{any_attribute::AnyAttribute, Attribute},
hydration::Cursor,
reactive_graph::OwnedView,
ssr::StreamBuilder,
@@ -163,6 +163,14 @@ where
self.children.insert_before_this(child)
}
}
fn elements(&self) -> Vec<tachys::renderer::types::Element> {
if let Some(fallback) = &self.fallback {
fallback.elements()
} else {
self.children.elements()
}
}
}
impl<Chil, FalFn, Fal> Render for ErrorBoundaryView<Chil, FalFn>
@@ -268,6 +276,7 @@ where
Fal: RenderHtml + Send + 'static,
{
type AsyncOutput = ErrorBoundaryView<Chil::AsyncOutput, FalFn>;
type Owned = Self;
const MIN_LENGTH: usize = Chil::MIN_LENGTH;
@@ -301,6 +310,7 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) {
// first, attempt to serialize the children to HTML, then check for errors
let _hook = throw_error::set_error_hook(self.hook);
@@ -311,6 +321,7 @@ where
&mut new_pos,
escape,
mark_branches,
extra_attrs.clone(),
);
// any thrown errors would've been caught here
@@ -323,6 +334,7 @@ where
position,
escape,
mark_branches,
extra_attrs,
);
}
}
@@ -333,6 +345,7 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) where
Self: Sized,
{
@@ -345,6 +358,7 @@ where
&mut new_pos,
escape,
mark_branches,
extra_attrs.clone(),
);
// any thrown errors would've been caught here
@@ -358,6 +372,7 @@ where
position,
escape,
mark_branches,
extra_attrs,
);
buf.push_sync(&fallback);
}
@@ -423,6 +438,10 @@ where
},
)
}
fn into_owned(self) -> Self::Owned {
self
}
}
#[derive(Debug)]

View File

@@ -6,7 +6,10 @@ use reactive_graph::{
traits::Set,
};
use std::hash::Hash;
use tachys::{reactive_graph::OwnedView, view::keyed::keyed};
use tachys::{
reactive_graph::OwnedView,
view::keyed::{keyed, SerializableKey},
};
/// Iterates over children and displays them, keyed by the `key` function given.
///
@@ -121,7 +124,7 @@ where
EF: Fn(T) -> N + Send + Clone + 'static,
N: IntoView + 'static,
KF: Fn(&T) -> K + Send + Clone + 'static,
K: Eq + Hash + 'static,
K: Eq + Hash + SerializableKey + 'static,
T: Send + 'static,
{
// this takes the owner of the For itself
@@ -195,7 +198,7 @@ where
EF: Fn(ReadSignal<usize>, T) -> N + Send + Clone + 'static,
N: IntoView + 'static,
KF: Fn(&T) -> K + Send + Clone + 'static,
K: Eq + Hash + 'static,
K: Eq + Hash + SerializableKey + 'static,
T: Send + 'static,
{
// this takes the owner of the For itself
@@ -218,6 +221,7 @@ where
};
move || keyed(each(), key.clone(), children.clone())
}
/*
#[cfg(test)]
mod tests {

View File

@@ -3,7 +3,11 @@ use leptos_dom::helpers::window;
use leptos_server::{ServerAction, ServerMultiAction};
use serde::de::DeserializeOwned;
use server_fn::{
client::Client, codec::PostUrl, request::ClientReq, ServerFn, ServerFnError,
client::Client,
codec::PostUrl,
error::{IntoAppError, ServerFnErrorErr},
request::ClientReq,
Http, ServerFn,
};
use tachys::{
either::Either,
@@ -71,7 +75,7 @@ use web_sys::{
/// ```
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
#[component]
pub fn ActionForm<ServFn>(
pub fn ActionForm<ServFn, OutputProtocol>(
/// The action from which to build the form.
action: ServerAction<ServFn>,
/// A [`NodeRef`] in which the `<form>` element should be stored.
@@ -82,7 +86,7 @@ pub fn ActionForm<ServFn>(
) -> impl IntoView
where
ServFn: DeserializeOwned
+ ServerFn<InputEncoding = PostUrl>
+ ServerFn<Protocol = Http<PostUrl, OutputProtocol>>
+ Clone
+ Send
+ Sync
@@ -121,9 +125,10 @@ where
"Error converting form field into server function \
arguments: {err:?}"
);
value.set(Some(Err(ServerFnError::Serialization(
value.set(Some(Err(ServerFnErrorErr::Serialization(
err.to_string(),
))));
)
.into_app_error())));
version.update(|n| *n += 1);
}
}
@@ -146,7 +151,7 @@ where
/// [`form`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form)
/// progressively enhanced to use client-side routing.
#[component]
pub fn MultiActionForm<ServFn>(
pub fn MultiActionForm<ServFn, OutputProtocol>(
/// The action from which to build the form.
action: ServerMultiAction<ServFn>,
/// A [`NodeRef`] in which the `<form>` element should be stored.
@@ -160,7 +165,7 @@ where
+ Sync
+ Clone
+ DeserializeOwned
+ ServerFn<InputEncoding = PostUrl>
+ ServerFn<Protocol = Http<PostUrl, OutputProtocol>>
+ 'static,
ServFn::Output: Send + Sync + 'static,
<<ServFn::Client as Client<ServFn::Error>>::Request as ClientReq<
@@ -187,9 +192,10 @@ where
action.dispatch(new_input);
}
Err(err) => {
action.dispatch_sync(Err(ServerFnError::Serialization(
action.dispatch_sync(Err(ServerFnErrorErr::Serialization(
err.to_string(),
)));
)
.into_app_error()));
}
}
};

View File

@@ -52,6 +52,8 @@
mod.hydrate();
hydrateIslands(document.body, mod);
});
window.__hydrateIsland = (el, id) => hydrateIsland(el, id, mod);
})
});
})

View File

@@ -0,0 +1,378 @@
let NAVIGATION = 0;
window.addEventListener("click", async (ev) => {
const req = clickToReq(ev);
if(!req) {
return;
}
ev.preventDefault();
await navigateToPage(req, true);
});
window.addEventListener("popstate", async (ev) => {
const req = new Request(window.location);
ev.preventDefault();
await navigateToPage(req, true, true);
});
window.addEventListener("submit", async (ev) => {
const req = submitToReq(ev);
if(!req) {
return;
}
ev.preventDefault();
await navigateToPage(req, true);
});
async function navigateToPage(
/** @type Request */
req,
/** @type bool */
useViewTransition,
/** @type bool */
replace
) {
NAVIGATION += 1;
const currentNav = NAVIGATION;
// add a custom header to indicate that we're on a subsequent navigation
req.headers.append("Islands-Router", "true");
// fetch the new page
const resp = await fetch(req);
const redirected = resp.redirected;
const htmlString = await resp.text();
if(NAVIGATION === currentNav) {
// The 'doc' variable now contains the parsed DOM
const transition = async () => {
try {
diffPages(htmlString);
for(const island of document.querySelectorAll("leptos-island")) {
if(!island.$$hydrated) {
__hydrateIsland(island, island.dataset.component);
island.$$hydrated = true;
}
}
} catch(e) {
console.error(e);
}
};
// Not all browsers support startViewTransition; see https://caniuse.com/?search=startViewTransition
if (useViewTransition && document.startViewTransition) {
await document.startViewTransition(transition);
} else {
await transition()
}
const url = redirected ? resp.url : req.url;
if(replace) {
window.history.replaceState(undefined, null, url);
} else {
window.history.pushState(undefined, null, url);
}
}
}
function clickToReq(ev) {
// confirm that this is an <a> that meets our requirements
if (
ev.defaultPrevented ||
ev.button !== 0 ||
ev.metaKey ||
ev.altKey ||
ev.ctrlKey ||
ev.shiftKey
)
return;
/** @type HTMLAnchorElement | undefined;*/
const a = ev
.composedPath()
.find(el => el instanceof Node && el.nodeName.toUpperCase() === "A");
if (!a) return;
const svg = a.namespaceURI === "http://www.w3.org/2000/svg";
const href = svg ? a.href.baseVal : a.href;
const target = svg ? a.target.baseVal : a.target;
if (target || (!href && !a.hasAttribute("state"))) return;
const rel = (a.getAttribute("rel") || "").split(/\s+/);
if (a.hasAttribute("download") || (rel?.includes("external"))) return;
const url = svg ? new URL(href, document.baseURI) : new URL(href);
if (
url.origin !== window.location.origin // ||
// TODO base
//(basePath && url.pathname && !url.pathname.toLowerCase().startsWith(basePath.toLowerCase()))
)
return;
return new Request(url);
}
function submitToReq(ev) {
event.preventDefault();
const target = ev.target;
/** @type HTMLFormElement */
let form;
if(target instanceof HTMLFormElement) {
form = target;
} else {
if(!target.form) {
return;
}
form = target.form;
}
const method = form.method.toUpperCase();
if(method !== "GET" && method !== "POST") {
return;
}
const url = new URL(form.action);
let path = url.pathname;
const requestInit = {};
const data = new FormData(form);
const params = new URLSearchParams();
for (const [key, value] of data.entries()) {
params.append(key, value);
}
requestInit.headers = {
Accept: "text/html"
};
if(method === "GET") {
path += `?${params.toString()}`;
}
else {
requestInit.method = "POST";
requestInit.body = params;
}
return new Request(
path,
requestInit
);
}
function diffPages(htmlString) {
// Use DOMParser to parse the HTML string
const parser = new DOMParser();
const doc = parser.parseFromString(htmlString, 'text/html');
diffRange(document, document, doc, doc);
}
function diffRange(oldDocument, oldRoot, newDocument, newRoot, oldEnd, newEnd) {
const oldDocWalker = oldDocument.createTreeWalker(oldRoot);
const newDocWalker = newDocument.createTreeWalker(newRoot);
let oldNode = oldDocWalker.currentNode;
let newNode = newDocWalker.currentNode;
while (oldDocWalker.nextNode() && newDocWalker.nextNode()) {
oldNode = oldDocWalker.currentNode;
newNode = newDocWalker.currentNode;
if (oldNode == oldEnd || newNode == newEnd) {
break;
}
// if the nodes are different, we need to replace the old with the new
// because of the typed view tree, this should never actually happen
if (oldNode.nodeType !== newNode.nodeType) {
oldNode.replaceWith(newNode);
}
// if it's a text node, just update the text with the new text
else if (oldNode.nodeType === Node.TEXT_NODE) {
oldNode.textContent = newNode.textContent;
}
// 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);
}
// we use comment "branch marker" nodes to distinguish between different branches in the statically-typed view tree
// if one of these marker is hit, then there are two options
// 1) it's the same branch, and we just keep walking until the end
// 2) it's a different branch, in which case the old can be replaced with the new wholesale
else if (oldNode.nodeType === Node.COMMENT_NODE) {
const oldText = oldNode.textContent;
const newText = newNode.textContent;
if(oldText.startsWith("bo-for")) {
replaceFor(oldDocument, oldDocWalker, newDocument, newDocWalker, oldNode, newNode);
}
else if (oldText.startsWith("bo-item")) {
// skip, this means we're diffing a new item within a For
}
else if(oldText.startsWith("bo") && newText !== oldText) {
replaceBranch(oldDocWalker, newDocWalker, oldNode, newNode);
}
}
}
}
function replaceFor(oldDocument, oldDocWalker, newDocument, newDocWalker, oldNode, newNode) {
oldDocWalker.nextNode();
newDocWalker.nextNode();
const oldRange = new Range();
const newRange = new Range();
let oldBranches = 1;
let newBranches = 1;
const oldKeys = {};
const newKeys = {};
while(oldBranches > 0) {
const c = oldDocWalker.currentNode;
if(c.nodeType === Node.COMMENT_NODE) {
const t = c.textContent;
if(t.startsWith("bo-for")) {
oldBranches += 1;
} else if(t.startsWith("bc-for")) {
oldBranches -= 1;
} else if (t.startsWith("bo-item")) {
const k = t.replace("bo-item-", "");
oldKeys[k] = { open: c, close: null };
} else if (t.startsWith("bc-item")) {
const k = t.replace("bc-item-", "");
oldKeys[k].close = c;
}
}
oldDocWalker.nextNode();
}
while(newBranches > 0) {
const c = newDocWalker.currentNode;
if(c.nodeType === Node.COMMENT_NODE) {
const t = c.textContent;
if(t.startsWith("bo-for")) {
newBranches += 1;
} else if(t.startsWith("bc-for")) {
newBranches -= 1;
} else if (t.startsWith("bo-item")) {
const k = t.replace("bo-item-", "");
newKeys[k] = { open: c, close: null };
} else if (t.startsWith("bc-item")) {
const k = t.replace("bc-item-", "");
newKeys[k].close = c;
}
}
newDocWalker.nextNode();
}
for(const key in oldKeys) {
if(newKeys[key]) {
const oldOne = oldKeys[key];
const newOne = newKeys[key];
const oldRange = new Range();
const newRange = new Range();
// then replace the item in the *new* list with the *old* DOM elements
oldRange.setStartAfter(oldOne.open);
oldRange.setEndBefore(oldOne.close);
newRange.setStartAfter(newOne.open);
newRange.setEndBefore(newOne.close);
const oldContents = oldRange.extractContents();
const newContents = newRange.extractContents();
// patch the *old* DOM elements with the new ones
diffRange(oldDocument, oldContents, newDocument, newContents, oldOne.close, newOne.close);
// then insert the old DOM elements into the new tree
// this means you'll end up with any new attributes or content from the server,
// but with any old DOM state (because they are the old elements)
newRange.insertNode(oldContents);
newOne.open.replaceWith(oldOne.open);
newOne.close.replaceWith(oldOne.close);
}
}
try {
oldRange.setStartAfter(oldNode);
oldRange.setEndBefore(oldDocWalker.currentNode);
newRange.setStartAfter(newNode);
newRange.setEndAfter(newDocWalker.currentNode);
const newContents = newRange.extractContents();
oldRange.deleteContents();
oldRange.insertNode(newContents);
oldNode.replaceWith(newNode);
oldDocWalker.currentNode.replaceWith(newDocWalker.currentNode);
} catch (e) {
console.error(e);
}
}
function replaceBranch(oldDocWalker, newDocWalker, oldNode, newNode) {
oldDocWalker.nextNode();
newDocWalker.nextNode();
const oldRange = new Range();
const newRange = new Range();
let oldBranches = 1;
let newBranches = 1;
while(oldBranches > 0) {
if(oldDocWalker.nextNode()) {
if(oldDocWalker.currentNode.nodeType === Node.COMMENT_NODE) {
if(oldDocWalker.currentNode.textContent.startsWith("bo")) {
oldBranches += 1;
} else if(oldDocWalker.currentNode.textContent.startsWith("bc")) {
oldBranches -= 1;
}
}
}
}
while(newBranches > 0) {
if(newDocWalker.nextNode()) {
if(newDocWalker.currentNode.nodeType === Node.COMMENT_NODE) {
if(newDocWalker.currentNode.textContent.startsWith("bo")) {
newBranches += 1;
} else if(newDocWalker.currentNode.textContent.startsWith("bc")) {
newBranches -= 1;
}
}
}
}
try {
oldRange.setStartAfter(oldNode);
oldRange.setEndBefore(oldDocWalker.currentNode);
newRange.setStartAfter(newNode);
newRange.setEndAfter(newDocWalker.currentNode);
const newContents = newRange.extractContents();
oldRange.deleteContents();
oldRange.insertNode(newContents);
oldNode.replaceWith(newNode);
oldDocWalker.currentNode.replaceWith(newDocWalker.currentNode);
} catch (e) {
console.error(e);
}
}
function diffElement(oldNode, newNode) {
/** @type Element */
const oldEl = oldNode;
/** @type Element */
const newEl = newNode;
if (oldEl.tagName !== newEl.tagName) {
oldEl.replaceWith(newEl);
}
else {
for(const attr of newEl.attributes) {
oldEl.setAttribute(attr.name, attr.value);
}
}
}
for(const island of document.querySelectorAll("leptos-island")) {
island.$$hydrated = true;
}

View File

@@ -50,6 +50,10 @@ pub fn HydrationScripts(
/// Should be `true` to hydrate in `islands` mode.
#[prop(optional)]
islands: bool,
/// Should be `true` to add the “islands router,” which enables limited client-side routing
/// when running in islands mode.
#[prop(optional)]
islands_router: bool,
/// A base url, not including a trailing slash
#[prop(optional, into)]
root: Option<String>,
@@ -98,18 +102,36 @@ pub fn HydrationScripts(
include_str!("./hydration_script.js")
};
let islands_router = islands_router
.then_some(include_str!("./islands_routing.js"))
.unwrap_or_default();
let root = root.unwrap_or_default();
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:?})")}
</script>
}
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>
}
})
}
/// If this is provided via context, it means that you are using the islands router and
/// this is a subsequent navigation, made from the client.
///
/// This should be provided automatically by a server integration if it detects that the
/// header `Islands-Router` is present in the request.
///
/// This is used to determine how much of the hydration script to include in the page.
/// If it is present, then the contents of the `<HydrationScripts>` component will not be
/// included, as they only need to be sent to the client once.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IslandsRouterNavigation;

View File

@@ -1,6 +1,6 @@
use std::borrow::Cow;
use tachys::{
html::attribute::Attribute,
html::attribute::{any_attribute::AnyAttribute, Attribute},
hydration::Cursor,
ssr::StreamBuilder,
view::{
@@ -87,6 +87,7 @@ impl<T: Render> Render for View<T> {
impl<T: RenderHtml> RenderHtml for View<T> {
type AsyncOutput = T::AsyncOutput;
type Owned = View<T::Owned>;
const MIN_LENGTH: usize = <T as RenderHtml>::MIN_LENGTH;
@@ -104,6 +105,7 @@ impl<T: RenderHtml> RenderHtml for View<T> {
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) {
#[cfg(debug_assertions)]
let vm = self.view_marker.to_owned();
@@ -112,8 +114,13 @@ impl<T: RenderHtml> RenderHtml for View<T> {
buf.push_str(&format!("<!--hot-reload|{vm}|open-->"));
}
self.inner
.to_html_with_buf(buf, position, escape, mark_branches);
self.inner.to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs,
);
#[cfg(debug_assertions)]
if let Some(vm) = vm.as_ref() {
@@ -127,6 +134,7 @@ impl<T: RenderHtml> RenderHtml for View<T> {
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) where
Self: Sized,
{
@@ -142,6 +150,7 @@ impl<T: RenderHtml> RenderHtml for View<T> {
position,
escape,
mark_branches,
extra_attrs,
);
#[cfg(debug_assertions)]
@@ -157,6 +166,14 @@ impl<T: RenderHtml> RenderHtml for View<T> {
) -> Self::State {
self.inner.hydrate::<FROM_SERVER>(cursor, position)
}
fn into_owned(self) -> Self::Owned {
View {
inner: self.inner.into_owned(),
#[cfg(debug_assertions)]
view_marker: self.view_marker,
}
}
}
impl<T: ToTemplate> ToTemplate for View<T> {

View File

@@ -172,12 +172,10 @@ pub mod prelude {
actions::*, computed::*, effect::*, graph::untrack, owner::*,
signal::*, wrappers::read::*,
};
pub use server_fn::{self, ServerFnError};
pub use server_fn::{self, error::ServerFnError};
pub use tachys::{
reactive_graph::{bind::BindAttribute, node_ref::*, Suspend},
view::{
any_view::AnyView, fragment::Fragment, template::ViewTemplate,
},
view::{fragment::Fragment, template::ViewTemplate},
};
}
pub use export_types::*;

View File

@@ -19,7 +19,7 @@ use slotmap::{DefaultKey, SlotMap};
use std::sync::Arc;
use tachys::{
either::Either,
html::attribute::Attribute,
html::attribute::{any_attribute::AnyAttribute, Attribute},
hydration::Cursor,
reactive_graph::{OwnedView, OwnedViewState},
ssr::StreamBuilder,
@@ -247,6 +247,7 @@ where
// i.e., if this is the child of another Suspense during SSR, don't wait for it: it will handle
// itself
type AsyncOutput = Self;
type Owned = Self;
const MIN_LENGTH: usize = Chil::MIN_LENGTH;
@@ -262,9 +263,15 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) {
self.fallback
.to_html_with_buf(buf, position, escape, mark_branches);
self.fallback.to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs,
);
}
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
@@ -273,6 +280,7 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) where
Self: Sized,
{
@@ -303,11 +311,13 @@ where
let eff = reactive_graph::effect::Effect::new_isomorphic({
move |_| {
tasks.track();
if tasks.read().is_empty() {
if let Some(tx) = tasks_tx.take() {
// If the receiver has dropped, it means the ScopedFuture has already
// dropped, so it doesn't matter if we manage to send this.
_ = tx.send(());
if let Some(tasks) = tasks.try_read() {
if tasks.is_empty() {
if let Some(tx) = tasks_tx.take() {
// If the receiver has dropped, it means the ScopedFuture has already
// dropped, so it doesn't matter if we manage to send this.
_ = tx.send(());
}
}
}
}
@@ -369,6 +379,7 @@ where
position,
escape,
mark_branches,
extra_attrs,
);
}
Some(None) => {
@@ -378,6 +389,7 @@ where
position,
escape,
mark_branches,
extra_attrs,
);
}
None => {
@@ -391,12 +403,14 @@ where
self.fallback,
&mut fallback_position,
mark_branches,
extra_attrs.clone(),
);
buf.push_async_out_of_order_with_nonce(
fut,
position,
mark_branches,
nonce_or_not(),
extra_attrs,
);
} else {
buf.push_async({
@@ -412,6 +426,7 @@ where
&mut position,
escape,
mark_branches,
extra_attrs,
);
builder.finish().take_chunks()
}
@@ -461,6 +476,10 @@ where
}
})
}
fn into_owned(self) -> Self::Owned {
self
}
}
/// A wrapper that prevents [`Suspense`] from waiting for any resource reads that happen inside
@@ -513,6 +532,7 @@ where
T: RenderHtml + 'static,
{
type AsyncOutput = Self;
type Owned = Self;
const MIN_LENGTH: usize = T::MIN_LENGTH;
@@ -528,8 +548,15 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) {
(self.0)().to_html_with_buf(buf, position, escape, mark_branches);
(self.0)().to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs,
);
}
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
@@ -538,6 +565,7 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) where
Self: Sized,
{
@@ -546,6 +574,7 @@ where
position,
escape,
mark_branches,
extra_attrs,
);
}
@@ -556,4 +585,8 @@ where
) -> Self::State {
(self.0)().hydrate::<FROM_SERVER>(cursor, position)
}
fn into_owned(self) -> Self::Owned {
self
}
}

View File

@@ -76,9 +76,9 @@ impl Default for TextProp {
}
impl IntoAttributeValue for TextProp {
type Output = Oco<'static, str>;
type Output = Arc<dyn Fn() -> Oco<'static, str> + Send + Sync>;
fn into_attribute_value(self) -> Self::Output {
self.get()
self.0
}
}

View File

@@ -10,7 +10,7 @@ rust-version.workspace = true
edition.workspace = true
[dependencies]
config = { version = "0.14.1", default-features = false, features = [
config = { version = "0.15.8", default-features = false, features = [
"toml",
"convert-case",
] }
@@ -20,7 +20,7 @@ thiserror = "2.0"
typed-builder = "0.20.0"
[dev-dependencies]
tokio = { version = "1.41", features = ["rt", "macros"] }
tokio = { version = "1.43", features = ["rt", "macros"] }
tempfile = "3.14"
temp-env = { version = "0.3.6", features = ["async_closure"] }
@@ -28,4 +28,4 @@ temp-env = { version = "0.3.6", features = ["async_closure"] }
rustdoc-args = ["--generate-link-to-definition"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }

View File

@@ -12,8 +12,9 @@ use typed_builder::TypedBuilder;
/// A Struct to allow us to parse LeptosOptions from the file. Not really needed, most interactions should
/// occur with LeptosOptions
#[derive(Clone, Debug, serde::Deserialize, Default)]
#[derive(Clone, Debug, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub struct ConfFile {
pub leptos_options: LeptosOptions,
}
@@ -24,9 +25,14 @@ pub struct ConfFile {
/// It shares keys with cargo-leptos, to allow for easy interoperability
#[derive(TypedBuilder, Debug, Clone, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub struct LeptosOptions {
/// The name of the WASM and JS files generated by wasm-bindgen. Defaults to the crate name with underscores instead of dashes
#[builder(setter(into), default=default_output_name())]
/// The name of the WASM and JS files generated by wasm-bindgen.
///
/// This should match the name that will be output when building your application.
///
/// You can easily set this using `env!("CARGO_CRATE_NAME")`.
#[builder(setter(into))]
pub output_name: Arc<str>,
/// The path of the all the files generated by cargo-leptos. This defaults to '.' for convenience when integrating with other
/// tools.
@@ -78,6 +84,40 @@ pub struct LeptosOptions {
#[builder(default = default_hash_files())]
#[serde(default = "default_hash_files")]
pub hash_files: bool,
/// The default prefix to use for server functions when generating API routes. Can be
/// overridden for individual functions using `#[server(prefix = "...")]` as usual.
///
/// This is useful to override the default prefix (`/api`) for all server functions without
/// needing to manually specify via `#[server(prefix = "...")]` on every server function.
#[builder(default, setter(strip_option))]
#[serde(default)]
pub server_fn_prefix: Option<String>,
/// Whether to disable appending the server functions' hashes to the end of their API names.
///
/// This is useful when an app's client side needs a stable server API. For example, shipping
/// the CSR WASM binary in a Tauri app. Tauri app releases are dependent on each platform's
/// distribution method (e.g., the Apple App Store or the Google Play Store), which typically
/// are much slower than the frequency at which a website can be updated. In addition, it's
/// common for users to not have the latest app version installed. In these cases, the CSR WASM
/// app would need to be able to continue calling the backend server function API, so the API
/// path needs to be consistent and not have a hash appended.
///
/// Note that the hash suffixes is intended as a way to ensure duplicate API routes are created.
/// Without the hash, server functions will need to have unique names to avoid creating
/// duplicate routes. Axum will throw an error if a duplicate route is added to the router, but
/// Actix will not.
#[builder(default)]
#[serde(default)]
pub disable_server_fn_hash: bool,
/// Include the module path of the server function in the API route. This is an alternative
/// strategy to prevent duplicate server function API routes (the default strategy is to add
/// a hash to the end of the route). Each element of the module path will be separated by a `/`.
/// For example, a server function with a fully qualified name of `parent::child::server_fn`
/// would have an API route of `/api/parent/child/server_fn` (possibly with a
/// different prefix and a hash suffix depending on the values of the other server fn configs).
#[builder(default)]
#[serde(default)]
pub server_fn_mod_path: bool,
}
impl LeptosOptions {
@@ -120,20 +160,14 @@ impl LeptosOptions {
hash_file: env_w_default("LEPTOS_HASH_FILE_NAME", "hash.txt")?
.into(),
hash_files: env_w_default("LEPTOS_HASH_FILES", "false")?.parse()?,
server_fn_prefix: env_wo_default("SERVER_FN_PREFIX")?,
disable_server_fn_hash: env_wo_default("DISABLE_SERVER_FN_HASH")?
.is_some(),
server_fn_mod_path: env_wo_default("SERVER_FN_MOD_PATH")?.is_some(),
})
}
}
impl Default for LeptosOptions {
fn default() -> Self {
LeptosOptions::builder().build()
}
}
fn default_output_name() -> Arc<str> {
env!("CARGO_CRATE_NAME").replace('-', "_").into()
}
fn default_site_root() -> Arc<str> {
".".into()
}

View File

@@ -15,7 +15,7 @@ or_poisoned = { workspace = true }
js-sys = "0.3.74"
send_wrapper = "0.6.0"
tracing = { version = "0.1.41", optional = true }
wasm-bindgen = "0.2.97"
wasm-bindgen = { workspace = true }
serde_json = { version = "1.0", optional = true }
serde = { version = "1.0", optional = true }
@@ -39,4 +39,4 @@ rustdoc-args = ["--generate-link-to-definition"]
denylist = ["tracing"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }

View File

@@ -16,7 +16,7 @@ proc-macro = true
attribute-derive = { version = "0.10.3", features = ["syn-full"] }
cfg-if = "1.0"
html-escape = "0.2.13"
itertools = "0.13.0"
itertools = { workspace = true }
prettyplease = "0.2.25"
proc-macro-error2 = { version = "2.0", default-features = false }
proc-macro2 = "1.0"
@@ -25,7 +25,7 @@ syn = { version = "2.0", features = ["full"] }
rstml = "0.12.0"
leptos_hot_reload = { workspace = true }
server_fn_macro = { workspace = true }
convert_case = "0.6.0"
convert_case = "0.7"
uuid = { version = "1.11", features = ["v4"] }
tracing = { version = "0.1.41", optional = true }
@@ -51,41 +51,42 @@ trace-component-props = []
actix = ["server_fn_macro/actix"]
axum = ["server_fn_macro/axum"]
generic = ["server_fn_macro/generic"]
# 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
# TLDR proc macros will ignore RUSTFLAGS when --target is specified on the cargo command.
# This works around the issue by the non proc-macro crate which does see RUSTFLAGS enabling the replacement feature on the proc-macro crate, which wouldn't.
# This is automatic as long as the leptos crate is depended upon,
# downstream usage should never manually enable this feature.
__internal_erase_components = []
[package.metadata.cargo-all-features]
denylist = ["nightly", "tracing", "trace-component-props"]
skip_feature_sets = [
[
"csr",
"hydrate",
],
[
"hydrate",
"csr",
],
[
"hydrate",
"ssr",
],
[
"actix",
"axum",
],
[
"actix",
"generic",
],
[
"generic",
"axum",
],
[
"csr",
"hydrate",
],
[
"hydrate",
"csr",
],
[
"hydrate",
"ssr",
],
[
"actix",
"axum",
],
[
"actix",
"generic",
],
[
"generic",
"axum",
],
]
[package.metadata.docs.rs]
rustdoc-args = ["--generate-link-to-definition"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = [
'cfg(leptos_debuginfo)',
'cfg(erase_components)',
] }

View File

@@ -11,13 +11,13 @@ dependencies = [
[tasks.test-leptos_macro-example]
description = "Tests the leptos_macro/example to check if macro handles doc comments correctly"
command = "cargo"
args = ["+nightly-2024-08-01", "test", "--doc"]
args = ["+nightly-2025-03-05", "test", "--doc"]
cwd = "example"
install_crate = false
[tasks.doc-leptos_macro-example]
description = "Docs the leptos_macro/example to check if macro handles doc comments correctly"
command = "cargo"
args = ["+nightly-2024-08-01", "doc"]
args = ["+nightly-2025-03-05", "doc"]
cwd = "example"
install_crate = false

View File

@@ -32,6 +32,8 @@ pub struct Model {
impl Parse for Model {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut item = ItemFn::parse(input)?;
maybe_modify_return_type(&mut item.sig.output);
convert_impl_trait_to_generic(&mut item.sig);
let docs = Docs::new(&item.attrs);
@@ -76,6 +78,39 @@ impl Parse for Model {
}
}
/// Exists to fix nested routes defined in a separate component in erased mode,
/// by replacing the return type with AnyNestedRoute, which is what it'll be, but is required as the return type for compiler inference.
fn maybe_modify_return_type(ret: &mut ReturnType) {
#[cfg(feature = "__internal_erase_components")]
{
if let ReturnType::Type(_, ty) = ret {
if let Type::ImplTrait(TypeImplTrait { bounds, .. }) = ty.as_ref() {
// If one of the bounds is MatchNestedRoutes, we need to replace the return type with AnyNestedRoute:
if bounds.iter().any(|bound| {
if let syn::TypeParamBound::Trait(trait_bound) = bound {
if trait_bound.path.segments.iter().any(
|path_segment| {
path_segment.ident == "MatchNestedRoutes"
},
) {
return true;
}
}
false
}) {
*ty = parse_quote!(
::leptos_router::any_nested_route::AnyNestedRoute
);
}
}
}
}
#[cfg(not(feature = "__internal_erase_components"))]
{
let _ = ret;
}
}
// implemented manually because Vec::drain_filter is nightly only
// follows std recommended parallel
pub fn drain_filter<T>(
@@ -296,9 +331,9 @@ impl ToTokens for Model {
let component = if *is_transparent {
body_expr
} else if cfg!(erase_components) {
} else if cfg!(feature = "__internal_erase_components") {
quote! {
::leptos::prelude::IntoAny::into_any(
::leptos::prelude::IntoMaybeErased::into_maybe_erased(
::leptos::reactive::graph::untrack_with_diagnostics(
move || {
#tracing_guard_expr
@@ -613,7 +648,8 @@ impl Parse for DummyModel {
drain_filter(&mut attrs, |attr| !attr.path().is_ident("doc"));
let vis: Visibility = input.parse()?;
let sig: Signature = input.parse()?;
let mut sig: Signature = input.parse()?;
maybe_modify_return_type(&mut sig.output);
// The body is left untouched, so it will not cause an error
// even if the syntax is invalid.

View File

@@ -281,7 +281,11 @@ pub fn view(tokens: TokenStream) -> TokenStream {
#[proc_macro]
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
pub fn template(tokens: TokenStream) -> TokenStream {
view_macro_impl(tokens, true)
if cfg!(feature = "__internal_erase_components") {
view(tokens)
} else {
view_macro_impl(tokens, true)
}
}
fn view_macro_impl(tokens: TokenStream, template: bool) -> TokenStream {
@@ -923,7 +927,7 @@ pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
args.into(),
s.into(),
Some(syn::parse_quote!(::leptos::server_fn)),
"/api",
option_env!("SERVER_FN_PREFIX").unwrap_or("/api"),
None,
None,
) {

View File

@@ -1,15 +1,19 @@
use super::{
fragment_to_tokens, utils::is_nostrip_optional_and_update_key, TagType,
};
use crate::view::{attribute_absolute, utils::filter_prefixed_attrs};
use crate::view::{
attribute_absolute, text_to_tokens, utils::filter_prefixed_attrs,
};
use proc_macro2::{Ident, TokenStream, TokenTree};
use quote::{format_ident, quote, quote_spanned};
use rstml::node::{
CustomNode, KeyedAttributeValue, NodeAttribute, NodeBlock, NodeElement,
NodeName,
CustomNode, KeyedAttributeValue, Node, NodeAttribute, NodeBlock,
NodeElement, NodeName,
};
use std::collections::HashMap;
use syn::{spanned::Spanned, Expr, ExprPath, ExprRange, RangeLimits, Stmt};
use syn::{
spanned::Spanned, Expr, ExprPath, ExprRange, Item, RangeLimits, Stmt,
};
pub(crate) fn component_to_tokens(
node: &mut NodeElement<impl CustomNode>,
@@ -170,8 +174,14 @@ pub(crate) fn component_to_tokens(
.collect::<Vec<_>>();
let spreads = (!(spreads.is_empty())).then(|| {
quote! {
.add_any_attr((#(#spreads,)*).into_attr())
if cfg!(feature = "__internal_erase_components") {
quote! {
.add_any_attr(vec![#(#spreads.into_any_attr(),)*])
}
} else {
quote! {
.add_any_attr((#(#spreads,)*))
}
}
});
@@ -191,6 +201,12 @@ pub(crate) fn component_to_tokens(
let mut slots = HashMap::new();
let children = if node.children.is_empty() {
quote! {}
} else if let Some(children) = maybe_optimised_component_children(
&node.children,
&items_to_bind,
&items_to_clone,
) {
children
} else {
let children = fragment_to_tokens(
&mut node.children,
@@ -219,10 +235,7 @@ pub(crate) fn component_to_tokens(
let bindables =
items_to_bind.iter().map(|ident| quote! { #ident, });
let clonables = items_to_clone.iter().map(|ident| {
let ident_ref = quote_spanned!(ident.span()=> &#ident);
quote! { let #ident = ::core::clone::Clone::clone(#ident_ref); }
});
let clonables = items_to_clone_to_tokens(&items_to_clone);
if bindables.len() > 0 {
quote_spanned! {children.span()=>
@@ -313,3 +326,111 @@ fn is_attr_let(key: &NodeName) -> bool {
false
}
}
pub fn items_to_clone_to_tokens<'a>(
items_to_clone: &'a [Ident],
) -> impl Iterator<Item = TokenStream> + 'a {
items_to_clone.iter().map(|ident| {
let ident_ref = quote_spanned!(ident.span()=> &#ident);
quote! { let #ident = ::core::clone::Clone::clone(#ident_ref); }
})
}
/// By default all children are placed in an outer closure || #children.
/// This is to work with all the variants of the leptos::children::ToChildren::to_children trait.
/// Strings are optimised to be passed without the wrapping closure, providing significant compile time and binary size improvements.
pub fn maybe_optimised_component_children(
children: &[Node<impl CustomNode>],
items_to_bind: &[TokenStream],
items_to_clone: &[Ident],
) -> Option<TokenStream> {
// If there are bindables will have to be in a closure:
if !items_to_bind.is_empty() {
return None;
}
// Filter out comments:
let mut children_iter = children
.iter()
.filter(|child| !matches!(child, Node::Comment(_)));
let children = if let Some(child) = children_iter.next() {
// If more than one child after filtering out comments, don't think we can optimise:
if children_iter.next().is_some() {
return None;
}
match child {
Node::Text(text) => text_to_tokens(&text.value),
Node::RawText(raw) => {
let text = raw.to_string_best();
let text = syn::LitStr::new(&text, raw.span());
text_to_tokens(&text)
}
// Specifically allow std macros that produce strings:
Node::Block(NodeBlock::ValidBlock(block)) => {
fn is_supported(mac: &syn::Macro) -> bool {
for string_macro in ["format", "include_str"] {
if mac.path.is_ident(string_macro) {
return true;
}
}
false
}
if block.stmts.len() > 1 {
return None;
} else if let Some(stmt) = block.stmts.first() {
match stmt {
Stmt::Macro(mac) => {
// eprintln!("Macro: {:?}", mac.mac.path);
if is_supported(&mac.mac) {
quote! { #block }
} else {
return None;
}
}
Stmt::Item(Item::Macro(mac)) => {
// eprintln!("Item Macro: {:?}", mac.mac.path);
if is_supported(&mac.mac) {
quote! { #block }
} else {
return None;
}
}
Stmt::Expr(Expr::Macro(mac), _) => {
// eprintln!("Expr Macro: {:?}", mac.mac.path);
if is_supported(&mac.mac) {
quote! { #block }
} else {
return None;
}
}
_ => return None,
}
} else {
return Some(quote! {});
}
}
_ => return None,
}
} else {
return None;
};
// // Debug check to see how many use this optimisation:
// static COUNT: std::sync::atomic::AtomicUsize =
// std::sync::atomic::AtomicUsize::new(0);
// COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
// eprintln!(
// "Optimised children: {}",
// COUNT.load(std::sync::atomic::Ordering::Relaxed)
// );
let clonables = items_to_clone_to_tokens(items_to_clone);
Some(quote_spanned! {children.span()=>
.children({
#(#clonables)*
::leptos::children::ToChildren::to_children(::leptos::children::ChildrenOptContainer(#children))
})
})
}

View File

@@ -428,6 +428,12 @@ fn element_children_to_tokens(
{ #child }
)
})
} else if cfg!(feature = "__internal_erase_components") {
Some(quote! {
.child(
leptos::tachys::view::iterators::StaticVec::from(vec![#(#children.into_maybe_erased()),*])
)
})
} else if children.len() > 16 {
// implementations of various traits used in routing and rendering are implemented for
// tuples of sizes 0, 1, 2, 3, ... N. N varies but is > 16. The traits are also implemented
@@ -473,6 +479,10 @@ fn fragment_to_tokens(
None
} else if children.len() == 1 {
children.into_iter().next()
} else if cfg!(feature = "__internal_erase_components") {
Some(quote! {
leptos::tachys::view::iterators::StaticVec::from(vec![#(#children.into_maybe_erased()),*])
})
} else if children.len() > 16 {
// implementations of various traits used in routing and rendering are implemented for
// tuples of sizes 0, 1, 2, 3, ... N. N varies but is > 16. The traits are also implemented
@@ -757,10 +767,18 @@ pub(crate) fn element_to_tokens(
}
}
}
Some(quote! {
(#(#attributes,)*)
#(.add_any_attr(#additions))*
})
if cfg!(feature = "__internal_erase_components") {
Some(quote! {
vec![#(#attributes.into_any_attr(),)*]
#(.add_any_attr(#additions))*
})
} else {
Some(quote! {
(#(#attributes,)*)
#(.add_any_attr(#additions))*
})
}
} else {
let tag = name.to_string();
// collect close_tag name to emit semantic information for IDE.
@@ -1157,8 +1175,14 @@ pub(crate) fn two_way_binding_to_tokens(
let ident =
format_ident!("{}", name.to_case(UpperCamel), span = node.key.span());
quote! {
.bind(::leptos::attr::#ident, #value)
if name == "group" {
quote! {
.bind(leptos::tachys::reactive_graph::bind::#ident, #value)
}
} else {
quote! {
.bind(::leptos::attr::#ident, #value)
}
}
}

View File

@@ -1,4 +1,7 @@
use super::{convert_to_snake_case, ident_from_tag_name};
use super::{
component_builder::maybe_optimised_component_children,
convert_to_snake_case, ident_from_tag_name,
};
use crate::view::{fragment_to_tokens, utils::filter_prefixed_attrs, TagType};
use proc_macro2::{Ident, TokenStream, TokenTree};
use quote::{quote, quote_spanned};
@@ -70,7 +73,10 @@ pub(crate) fn slot_to_tokens(
}
});
let items_to_bind = filter_prefixed_attrs(attrs.iter(), "let:");
let items_to_bind = filter_prefixed_attrs(attrs.iter(), "let:")
.into_iter()
.map(|ident| quote! { #ident })
.collect::<Vec<_>>();
let items_to_clone = filter_prefixed_attrs(attrs.iter(), "clone:");
@@ -83,7 +89,7 @@ pub(crate) fn slot_to_tokens(
let value = attr.value().map(|v| {
quote! { #v }
})?;
Some(quote! { (#name, ::leptos::IntoAttribute::into_attribute(#value)) })
Some(quote! { (#name, #value) })
})
.collect::<Vec<_>>();
@@ -96,6 +102,12 @@ pub(crate) fn slot_to_tokens(
let mut slots = HashMap::new();
let children = if node.children.is_empty() {
quote! {}
} else if let Some(children) = maybe_optimised_component_children(
&node.children,
&items_to_bind,
&items_to_clone,
) {
children
} else {
let children = fragment_to_tokens(
&mut node.children,

View File

@@ -1,9 +1,8 @@
#[cfg(not(feature = "ssr"))]
pub mod tests {
use leptos::{
server,
server_fn::{codec, ServerFn, ServerFnError},
server_fn::{codec, Http, ServerFn, ServerFnError},
};
use std::any::TypeId;
@@ -19,8 +18,8 @@ pub mod tests {
"/api/my_server_action"
);
assert_eq!(
TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(),
TypeId::of::<codec::PostUrl>()
TypeId::of::<<MyServerAction as ServerFn>::Protocol>(),
TypeId::of::<Http<codec::PostUrl, codec::Json>>()
);
}
@@ -32,8 +31,8 @@ pub mod tests {
}
assert_eq!(<FooBar as ServerFn>::PATH, "/foo/bar/my_path");
assert_eq!(
TypeId::of::<<FooBar as ServerFn>::InputEncoding>(),
TypeId::of::<codec::Cbor>()
TypeId::of::<<FooBar as ServerFn>::Protocol>(),
TypeId::of::<Http<codec::Cbor, codec::Cbor>>()
);
}
@@ -45,8 +44,8 @@ pub mod tests {
}
assert_eq!(<FooBar as ServerFn>::PATH, "/foo/bar/my_path");
assert_eq!(
TypeId::of::<<FooBar as ServerFn>::InputEncoding>(),
TypeId::of::<codec::Cbor>()
TypeId::of::<<FooBar as ServerFn>::Protocol>(),
TypeId::of::<Http<codec::Cbor, codec::Cbor>>()
);
}
@@ -58,8 +57,8 @@ pub mod tests {
}
assert_eq!(<FooBar as ServerFn>::PATH, "/api/my_path");
assert_eq!(
TypeId::of::<<FooBar as ServerFn>::InputEncoding>(),
TypeId::of::<codec::PostUrl>()
TypeId::of::<<FooBar as ServerFn>::Protocol>(),
TypeId::of::<Http<codec::PostUrl, codec::Json>>()
);
}
@@ -74,8 +73,8 @@ pub mod tests {
"/api/my_server_action"
);
assert_eq!(
TypeId::of::<<FooBar as ServerFn>::InputEncoding>(),
TypeId::of::<codec::PostUrl>()
TypeId::of::<<FooBar as ServerFn>::Protocol>(),
TypeId::of::<Http<codec::PostUrl, codec::Json>>()
);
}
@@ -91,8 +90,8 @@ pub mod tests {
"/foo/bar/my_server_action"
);
assert_eq!(
TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(),
TypeId::of::<codec::PostUrl>()
TypeId::of::<<MyServerAction as ServerFn>::Protocol>(),
TypeId::of::<Http<codec::PostUrl, codec::Json>>()
);
}
@@ -108,8 +107,8 @@ pub mod tests {
"/api/my_server_action"
);
assert_eq!(
TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(),
TypeId::of::<codec::GetUrl>()
TypeId::of::<<MyServerAction as ServerFn>::Protocol>(),
TypeId::of::<Http<codec::GetUrl, codec::Json>>()
);
}
@@ -124,8 +123,8 @@ pub mod tests {
"/api/path/to/my/endpoint"
);
assert_eq!(
TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(),
TypeId::of::<codec::PostUrl>()
TypeId::of::<<MyServerAction as ServerFn>::Protocol>(),
TypeId::of::<Http<codec::PostUrl, codec::Json>>()
);
}
}

View File

@@ -1,4 +1,4 @@
#[cfg(not(erase_components))]
#[cfg(not(feature = "__internal_erase_components"))]
#[test]
fn ui() {
let t = trybuild::TestCases::new();

View File

@@ -1,6 +1,8 @@
[package]
name = "leptos_server"
version = { workspace = true }
# TODO revert to { workspace = true } before 0.8.0 release
# this is a hack because I missing bumping the hydration_context version number before publishing
version = "0.8.0-beta"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
@@ -26,8 +28,8 @@ send_wrapper = "0.6"
# serialization formats
serde = { version = "1.0" }
js-sys = { version = "0.3.74", optional = true }
wasm-bindgen = { version = "0.2.97", optional = true }
serde_json = { version = "1.0" }
wasm-bindgen = { version = "0.2.100", optional = true }
serde_json = { workspace = true }
[features]
ssr = []
@@ -46,4 +48,4 @@ denylist = ["tracing"]
rustdoc-args = ["--generate-link-to-definition"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }

View File

@@ -3,7 +3,7 @@ use reactive_graph::{
owner::use_context,
traits::DefinedAt,
};
use server_fn::{error::ServerFnErrorSerde, ServerFn, ServerFnError};
use server_fn::{error::FromServerFnError, ServerFn};
use std::{ops::Deref, panic::Location, sync::Arc};
/// An error that can be caused by a server action.
@@ -42,7 +42,7 @@ where
S: ServerFn + 'static,
S::Output: 'static,
{
inner: ArcAction<S, Result<S::Output, ServerFnError<S::Error>>>,
inner: ArcAction<S, Result<S::Output, S::Error>>,
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
}
@@ -52,13 +52,14 @@ where
S: ServerFn + Clone + Send + Sync + 'static,
S::Output: Send + Sync + 'static,
S::Error: Send + Sync + 'static,
S::Error: FromServerFnError,
{
/// Creates a new [`ArcAction`] that will call the server function `S` when dispatched.
#[track_caller]
pub fn new() -> Self {
let err = use_context::<ServerActionError>().and_then(|error| {
(error.path() == S::PATH)
.then(|| ServerFnError::<S::Error>::de(error.err()))
.then(|| S::Error::de(error.err()))
.map(Err)
});
Self {
@@ -76,7 +77,7 @@ where
S: ServerFn + 'static,
S::Output: 'static,
{
type Target = ArcAction<S, Result<S::Output, ServerFnError<S::Error>>>;
type Target = ArcAction<S, Result<S::Output, S::Error>>;
fn deref(&self) -> &Self::Target {
&self.inner
@@ -131,7 +132,7 @@ where
S: ServerFn + 'static,
S::Output: 'static,
{
inner: Action<S, Result<S::Output, ServerFnError<S::Error>>>,
inner: Action<S, Result<S::Output, S::Error>>,
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
}
@@ -146,7 +147,7 @@ where
pub fn new() -> Self {
let err = use_context::<ServerActionError>().and_then(|error| {
(error.path() == S::PATH)
.then(|| ServerFnError::<S::Error>::de(error.err()))
.then(|| S::Error::de(error.err()))
.map(Err)
});
Self {
@@ -182,15 +183,14 @@ where
S::Output: Send + Sync + 'static,
S::Error: Send + Sync + 'static,
{
type Target = Action<S, Result<S::Output, ServerFnError<S::Error>>>;
type Target = Action<S, Result<S::Output, S::Error>>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl<S> From<ServerAction<S>>
for Action<S, Result<S::Output, ServerFnError<S::Error>>>
impl<S> From<ServerAction<S>> for Action<S, Result<S::Output, S::Error>>
where
S: ServerFn + 'static,
S::Output: 'static,

View File

@@ -79,7 +79,7 @@ mod view_implementations {
use reactive_graph::traits::Read;
use std::future::Future;
use tachys::{
html::attribute::Attribute,
html::attribute::{any_attribute::AnyAttribute, Attribute},
hydration::Cursor,
reactive_graph::{RenderEffectState, Suspend, SuspendState},
ssr::StreamBuilder,
@@ -135,6 +135,7 @@ mod view_implementations {
Ser: Send + 'static,
{
type AsyncOutput = Option<T>;
type Owned = Self;
const MIN_LENGTH: usize = 0;
@@ -152,12 +153,14 @@ mod view_implementations {
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) {
(move || Suspend::new(async move { self.await })).to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs,
);
}
@@ -167,6 +170,7 @@ mod view_implementations {
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) where
Self: Sized,
{
@@ -176,6 +180,7 @@ mod view_implementations {
position,
escape,
mark_branches,
extra_attrs,
);
}
@@ -187,5 +192,9 @@ mod view_implementations {
(move || Suspend::new(async move { self.await }))
.hydrate::<FROM_SERVER>(cursor, position)
}
fn into_owned(self) -> Self::Owned {
self
}
}
}

View File

@@ -8,13 +8,15 @@ use reactive_graph::{
ToAnySource, ToAnySubscriber,
},
owner::use_context,
send_wrapper_ext::MaybeSendWrapperOption,
signal::{
guards::{AsyncPlain, ReadGuard},
guards::{AsyncPlain, Mapped, ReadGuard},
ArcRwSignal, RwSignal,
},
traits::{DefinedAt, IsDisposed, ReadUntracked, Track, Update, Write},
traits::{
DefinedAt, IsDisposed, ReadUntracked, Track, Update, With, Write,
},
};
use send_wrapper::SendWrapper;
use std::{
future::{pending, Future, IntoFuture},
panic::Location,
@@ -22,7 +24,7 @@ use std::{
/// A reference-counted resource that only loads its data locally on the client.
pub struct ArcLocalResource<T> {
data: ArcAsyncDerived<SendWrapper<T>>,
data: ArcAsyncDerived<T>,
refetch: ArcRwSignal<usize>,
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
@@ -69,14 +71,12 @@ impl<T> ArcLocalResource<T> {
}
}
};
let fetcher = SendWrapper::new(fetcher);
let refetch = ArcRwSignal::new(0);
let data = {
let refetch = refetch.clone();
ArcAsyncDerived::new(move || {
ArcAsyncDerived::new_unsync(move || {
refetch.track();
let fut = fetcher();
SendWrapper::new(async move { SendWrapper::new(fut.await) })
fetcher()
})
};
Self {
@@ -91,6 +91,34 @@ impl<T> ArcLocalResource<T> {
pub fn refetch(&self) {
*self.refetch.write() += 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> ArcLocalResource<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 ArcLocalResource<T>
@@ -98,14 +126,9 @@ where
T: Clone + 'static,
{
type Output = T;
type IntoFuture = futures::future::Map<
AsyncDerivedFuture<SendWrapper<T>>,
fn(SendWrapper<T>) -> T,
>;
type IntoFuture = AsyncDerivedFuture<T>;
fn into_future(self) -> Self::IntoFuture {
use futures::FutureExt;
if let Some(mut notifier) = use_context::<LocalResourceNotifier>() {
notifier.notify();
} else if cfg!(feature = "ssr") {
@@ -115,7 +138,7 @@ where
always pending on the server."
);
}
self.data.into_future().map(|value| (*value).clone())
self.data.into_future()
}
}
@@ -136,18 +159,14 @@ impl<T> ReadUntracked for ArcLocalResource<T>
where
T: 'static,
{
type Value =
ReadGuard<Option<SendWrapper<T>>, AsyncPlain<Option<SendWrapper<T>>>>;
type Value = ReadGuard<
Option<T>,
Mapped<AsyncPlain<MaybeSendWrapperOption<T>>, Option<T>>,
>;
fn try_read_untracked(&self) -> Option<Self::Value> {
if let Some(mut notifier) = use_context::<LocalResourceNotifier>() {
notifier.notify();
} else if cfg!(feature = "ssr") {
panic!(
"Reading from a LocalResource outside Suspense in `ssr` mode \
will cause the response to hang, because LocalResources are \
always pending on the server."
);
}
self.data.try_read_untracked()
}
@@ -216,7 +235,7 @@ impl<T> Subscriber for ArcLocalResource<T> {
/// A resource that only loads its data locally on the client.
pub struct LocalResource<T> {
data: AsyncDerived<SendWrapper<T>>,
data: AsyncDerived<T>,
refetch: RwSignal<usize>,
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
@@ -266,11 +285,9 @@ impl<T> LocalResource<T> {
data: if cfg!(feature = "ssr") {
AsyncDerived::new_mock(fetcher)
} else {
let fetcher = SendWrapper::new(fetcher);
AsyncDerived::new(move || {
AsyncDerived::new_unsync_threadsafe_storage(move || {
refetch.track();
let fut = fetcher();
SendWrapper::new(async move { SendWrapper::new(fut.await) })
fetcher()
})
},
refetch,
@@ -290,14 +307,9 @@ where
T: Clone + 'static,
{
type Output = T;
type IntoFuture = futures::future::Map<
AsyncDerivedFuture<SendWrapper<T>>,
fn(SendWrapper<T>) -> T,
>;
type IntoFuture = AsyncDerivedFuture<T>;
fn into_future(self) -> Self::IntoFuture {
use futures::FutureExt;
if let Some(mut notifier) = use_context::<LocalResourceNotifier>() {
notifier.notify();
} else if cfg!(feature = "ssr") {
@@ -307,7 +319,7 @@ where
always pending on the server."
);
}
self.data.into_future().map(|value| (*value).clone())
self.data.into_future()
}
}
@@ -328,18 +340,14 @@ impl<T> ReadUntracked for LocalResource<T>
where
T: 'static,
{
type Value =
ReadGuard<Option<SendWrapper<T>>, AsyncPlain<Option<SendWrapper<T>>>>;
type Value = ReadGuard<
Option<T>,
Mapped<AsyncPlain<MaybeSendWrapperOption<T>>, Option<T>>,
>;
fn try_read_untracked(&self) -> Option<Self::Value> {
if let Some(mut notifier) = use_context::<LocalResourceNotifier>() {
notifier.notify();
} else if cfg!(feature = "ssr") {
panic!(
"Reading from a LocalResource outside Suspense in `ssr` mode \
will cause the response to hang, because LocalResources are \
always pending on the server."
);
}
self.data.try_read_untracked()
}

View File

@@ -2,7 +2,7 @@ use reactive_graph::{
actions::{ArcMultiAction, MultiAction},
traits::DefinedAt,
};
use server_fn::{ServerFn, ServerFnError};
use server_fn::ServerFn;
use std::{ops::Deref, panic::Location};
/// An [`ArcMultiAction`] that can be used to call a server function.
@@ -11,7 +11,7 @@ where
S: ServerFn + 'static,
S::Output: 'static,
{
inner: ArcMultiAction<S, Result<S::Output, ServerFnError<S::Error>>>,
inner: ArcMultiAction<S, Result<S::Output, S::Error>>,
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
}
@@ -40,7 +40,7 @@ where
S: ServerFn + 'static,
S::Output: 'static,
{
type Target = ArcMultiAction<S, Result<S::Output, ServerFnError<S::Error>>>;
type Target = ArcMultiAction<S, Result<S::Output, S::Error>>;
fn deref(&self) -> &Self::Target {
&self.inner
@@ -95,13 +95,13 @@ where
S: ServerFn + 'static,
S::Output: 'static,
{
inner: MultiAction<S, Result<S::Output, ServerFnError<S::Error>>>,
inner: MultiAction<S, Result<S::Output, S::Error>>,
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
}
impl<S> From<ServerMultiAction<S>>
for MultiAction<S, Result<S::Output, ServerFnError<S::Error>>>
for MultiAction<S, Result<S::Output, S::Error>>
where
S: ServerFn + 'static,
S::Output: 'static,
@@ -152,7 +152,7 @@ where
S::Output: 'static,
S::Error: 'static,
{
type Target = MultiAction<S, Result<S::Output, ServerFnError<S::Error>>>;
type Target = MultiAction<S, Result<S::Output, S::Error>>;
fn deref(&self) -> &Self::Target {
&self.inner

View File

@@ -168,6 +168,41 @@ where
data
}
/// 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: Send + Sync + 'static,
{
self.try_with(|n| n.as_ref().map(f))?
}
}
impl<T, E, Ser> ArcOnceResource<Result<T, E>, Ser>
where
Ser: Encoder<Result<T, E>> + Decoder<Result<T, E>>,
<Ser as Encoder<Result<T, E>>>::Error: Debug,
<Ser as Decoder<Result<T, E>>>::Error: Debug,
<<Ser as Decoder<Result<T, E>>>::Encoded as FromEncodedStr>::DecodingError:
Debug,
<Ser as Encoder<Result<T, E>>>::Encoded: IntoEncodedString,
<Ser as Decoder<Result<T, E>>>::Encoded: FromEncodedStr,
T: Send + Sync + 'static,
E: Send + Sync + 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, Ser> ArcOnceResource<T, Ser> {
@@ -534,6 +569,37 @@ where
defined_at,
}
}
/// Synchronously, reactively reads the current value of the resource and applies the function
/// `f` to its value if it is `Some(_)`.
pub fn map<U>(&self, f: impl FnOnce(&T) -> U) -> Option<U> {
self.try_with(|n| n.as_ref().map(|n| Some(f(n))))?.flatten()
}
}
impl<T, E, Ser> OnceResource<Result<T, E>, Ser>
where
Ser: Encoder<Result<T, E>> + Decoder<Result<T, E>>,
<Ser as Encoder<Result<T, E>>>::Error: Debug,
<Ser as Decoder<Result<T, E>>>::Error: Debug,
<<Ser as Decoder<Result<T, E>>>::Encoded as FromEncodedStr>::DecodingError:
Debug,
<Ser as Encoder<Result<T, E>>>::Encoded: IntoEncodedString,
<Ser as Decoder<Result<T, E>>>::Encoded: FromEncodedStr,
T: Send + Sync + 'static,
E: Send + Sync + 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, Ser> OnceResource<T, Ser>

View File

@@ -215,16 +215,11 @@ where
None
}
Ok(encoded) => {
match Ser::decode(encoded.borrow()) {
#[allow(unused_variables)]
// used in tracing
Err(e) => {
#[cfg(feature = "tracing")]
tracing::error!("{e:?}");
None
}
Ok(value) => Some(value),
}
let decoded = Ser::decode(encoded.borrow());
#[cfg(feature = "tracing")]
let decoded = decoded
.inspect_err(|e| tracing::error!("{e:?}"));
decoded.ok()
}
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_meta"
version = "0.7.7"
version = "0.8.0-beta"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
@@ -15,7 +15,7 @@ or_poisoned = { workspace = true }
indexmap = "2.6"
send_wrapper = "0.6.0"
tracing = { version = "0.1.41", optional = true }
wasm-bindgen = "0.2.97"
wasm-bindgen = { workspace = true }
futures = "0.3.31"
[dependencies.web-sys]

View File

@@ -1,6 +1,6 @@
use crate::ServerMetaContext;
use leptos::{
attr::NextAttribute,
attr::{any_attribute::AnyAttribute, NextAttribute},
component, html,
reactive::owner::use_context,
tachys::{
@@ -103,6 +103,7 @@ where
At: Attribute,
{
type AsyncOutput = BodyView<At::AsyncOutput>;
type Owned = BodyView<At::CloneableOwned>;
const MIN_LENGTH: usize = At::MIN_LENGTH;
@@ -122,10 +123,14 @@ where
_position: &mut Position,
_escape: bool,
_mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) {
if let Some(meta) = use_context::<ServerMetaContext>() {
let mut buf = String::new();
_ = html::attributes_to_html(self.attributes, &mut buf);
_ = html::attributes_to_html(
(self.attributes, extra_attrs),
&mut buf,
);
if !buf.is_empty() {
_ = meta.body.send(buf);
}
@@ -142,6 +147,12 @@ where
BodyViewState { attributes }
}
fn into_owned(self) -> Self::Owned {
BodyView {
attributes: self.attributes.into_cloneable_owned(),
}
}
}
impl<At> Mountable for BodyViewState<At>
@@ -160,4 +171,11 @@ where
fn insert_before_this(&self, _child: &mut dyn Mountable) -> bool {
false
}
fn elements(&self) -> Vec<leptos::tachys::renderer::types::Element> {
vec![document()
.body()
.expect("there to be a <body> element")
.into()]
}
}

View File

@@ -1,6 +1,6 @@
use crate::ServerMetaContext;
use leptos::{
attr::NextAttribute,
attr::{any_attribute::AnyAttribute, NextAttribute},
component, html,
reactive::owner::use_context,
tachys::{
@@ -103,6 +103,7 @@ where
At: Attribute,
{
type AsyncOutput = HtmlView<At::AsyncOutput>;
type Owned = HtmlView<At::CloneableOwned>;
const MIN_LENGTH: usize = At::MIN_LENGTH;
@@ -122,10 +123,14 @@ where
_position: &mut Position,
_escape: bool,
_mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) {
if let Some(meta) = use_context::<ServerMetaContext>() {
let mut buf = String::new();
_ = html::attributes_to_html(self.attributes, &mut buf);
_ = html::attributes_to_html(
(self.attributes, extra_attrs),
&mut buf,
);
if !buf.is_empty() {
_ = meta.html.send(buf);
}
@@ -145,6 +150,12 @@ where
HtmlViewState { attributes }
}
fn into_owned(self) -> Self::Owned {
HtmlView {
attributes: self.attributes.into_cloneable_owned(),
}
}
}
impl<At> Mountable for HtmlViewState<At>
@@ -165,4 +176,10 @@ where
fn insert_before_this(&self, _child: &mut dyn Mountable) -> bool {
false
}
fn elements(&self) -> Vec<leptos::tachys::renderer::types::Element> {
vec![document()
.document_element()
.expect("there to be a <html> element")]
}
}

View File

@@ -44,7 +44,7 @@
use futures::{Stream, StreamExt};
use leptos::{
attr::NextAttribute,
attr::{any_attribute::AnyAttribute, NextAttribute},
component,
logging::debug_warn,
oco::Oco,
@@ -323,37 +323,13 @@ pub(crate) fn register<E, At, Ch>(
where
HtmlElement<E, At, Ch>: RenderHtml,
{
#[allow(unused_mut)] // used for `ssr`
let mut el = Some(el);
#[cfg(feature = "ssr")]
if let Some(cx) = use_context::<ServerMetaContext>() {
let mut buf = String::new();
el.take().unwrap().to_html_with_buf(
&mut buf,
&mut Position::NextChild,
false,
false,
);
_ = cx.elements.send(buf); // fails only if the receiver is already dropped
} else {
let msg = "tried to use a leptos_meta component without \
`ServerMetaContext` provided";
#[cfg(feature = "tracing")]
tracing::warn!("{}", msg);
#[cfg(not(feature = "tracing"))]
eprintln!("{}", msg);
}
RegisteredMetaTag { el }
}
struct RegisteredMetaTag<E, At, Ch> {
// this is `None` if we've already taken it out to render to HTML on the server
// we don't render it in place in RenderHtml, so it's fine
el: Option<HtmlElement<E, At, Ch>>,
el: HtmlElement<E, At, Ch>,
}
struct RegisteredMetaTagState<E, At, Ch>
@@ -391,12 +367,12 @@ where
type State = RegisteredMetaTagState<E, At, Ch>;
fn build(self) -> Self::State {
let state = self.el.unwrap().build();
let state = self.el.build();
RegisteredMetaTagState { state }
}
fn rebuild(self, state: &mut Self::State) {
self.el.unwrap().rebuild(&mut state.state);
self.el.rebuild(&mut state.state);
}
}
@@ -417,7 +393,7 @@ where
Self::Output<NewAttr>: RenderHtml,
{
RegisteredMetaTag {
el: self.el.map(|inner| inner.add_any_attr(attr)),
el: self.el.add_any_attr(attr),
}
}
}
@@ -429,6 +405,7 @@ where
Ch: RenderHtml + Send,
{
type AsyncOutput = Self;
type Owned = RegisteredMetaTag<E, At::CloneableOwned, Ch::Owned>;
const MIN_LENGTH: usize = 0;
@@ -446,9 +423,31 @@ where
_position: &mut Position,
_escape: bool,
_mark_branches: bool,
_extra_attrs: Vec<AnyAttribute>,
) {
// meta tags are rendered into the buffer stored into the context
// the value has already been taken out, when we're on the server
#[cfg(feature = "ssr")]
if let Some(cx) = use_context::<ServerMetaContext>() {
let mut buf = String::new();
self.el.to_html_with_buf(
&mut buf,
&mut Position::NextChild,
false,
false,
vec![],
);
_ = cx.elements.send(buf); // fails only if the receiver is already dropped
} else {
let msg = "tried to use a leptos_meta component without \
`ServerMetaContext` provided";
#[cfg(feature = "tracing")]
tracing::warn!("{}", msg);
#[cfg(not(feature = "tracing"))]
eprintln!("{}", msg);
}
}
fn hydrate<const FROM_SERVER: bool>(
@@ -462,12 +461,18 @@ where
MetaContext provided",
)
.cursor;
let state = self.el.unwrap().hydrate::<FROM_SERVER>(
let state = self.el.hydrate::<FROM_SERVER>(
&cursor,
&PositionState::new(Position::NextChild),
);
RegisteredMetaTagState { state }
}
fn into_owned(self) -> Self::Owned {
RegisteredMetaTag {
el: self.el.into_owned(),
}
}
}
impl<E, At, Ch> Mountable for RegisteredMetaTagState<E, At, Ch>
@@ -500,6 +505,10 @@ where
// we intended!
false
}
fn elements(&self) -> Vec<leptos::tachys::renderer::types::Element> {
self.state.elements()
}
}
/// During server rendering, inserts the meta tags that have been generated by the other components
@@ -541,6 +550,7 @@ impl AddAnyAttr for MetaTagsView {
impl RenderHtml for MetaTagsView {
type AsyncOutput = Self;
type Owned = Self;
const MIN_LENGTH: usize = 0;
@@ -556,6 +566,7 @@ impl RenderHtml for MetaTagsView {
_position: &mut Position,
_escape: bool,
_mark_branches: bool,
_extra_attrs: Vec<AnyAttribute>,
) {
buf.push_str("<!--HEAD-->");
}
@@ -566,6 +577,10 @@ impl RenderHtml for MetaTagsView {
_position: &PositionState,
) -> Self::State {
}
fn into_owned(self) -> Self::Owned {
self
}
}
pub(crate) trait OrDefaultNonce {

View File

@@ -36,6 +36,10 @@ pub fn Stylesheet(
}
/// Injects an [`HTMLLinkElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement) into the document head that loads a `cargo-leptos`-hashed stylesheet.
///
/// This should only be used in the applications server-side `shell` function, as
/// [`LeptosOptions`] is not available in the browser. Unlike other `leptos_meta` components, it
/// will render the `<link>` it creates exactly where it is called.
#[component]
pub fn HashedStylesheet(
/// Leptos options
@@ -74,11 +78,9 @@ pub fn HashedStylesheet(
css_file_name.push_str(".css");
let pkg_path = &options.site_pkg_dir;
let root = root.unwrap_or_default();
// TODO additional attributes
register(
link()
.id(id)
.rel("stylesheet")
.href(format!("{root}/{pkg_path}/{css_file_name}")),
)
link()
.id(id)
.rel("stylesheet")
.href(format!("{root}/{pkg_path}/{css_file_name}"))
}

View File

@@ -1,6 +1,6 @@
use crate::{use_head, MetaContext, ServerMetaContext};
use leptos::{
attr::Attribute,
attr::{any_attribute::AnyAttribute, Attribute},
component,
oco::Oco,
reactive::{
@@ -234,6 +234,7 @@ impl AddAnyAttr for TitleView {
impl RenderHtml for TitleView {
type AsyncOutput = Self;
type Owned = Self;
const MIN_LENGTH: usize = 0;
@@ -249,6 +250,7 @@ impl RenderHtml for TitleView {
_position: &mut Position,
_escape: bool,
_mark_branches: bool,
_extra_attrs: Vec<AnyAttribute>,
) {
// meta tags are rendered into the buffer stored into the context
// the value has already been taken out, when we're on the server
@@ -282,6 +284,10 @@ impl RenderHtml for TitleView {
});
TitleViewState { effect }
}
fn into_owned(self) -> Self::Owned {
self
}
}
impl Mountable for TitleViewState {
@@ -299,4 +305,8 @@ impl Mountable for TitleViewState {
fn insert_before_this(&self, _child: &mut dyn Mountable) -> bool {
false
}
fn elements(&self) -> Vec<leptos::tachys::renderer::types::Element> {
vec![]
}
}

View File

@@ -13,4 +13,4 @@ serde = "1.0"
thiserror = "2.0"
[dev-dependencies]
serde_json = "1.0"
serde_json = { workspace = true }

View File

@@ -35,7 +35,7 @@ pub trait OrPoisoned {
fn or_poisoned(self) -> Self::Inner;
}
impl<'a, T> OrPoisoned
impl<'a, T: ?Sized> OrPoisoned
for Result<RwLockReadGuard<'a, T>, PoisonError<RwLockReadGuard<'a, T>>>
{
type Inner = RwLockReadGuard<'a, T>;
@@ -45,7 +45,7 @@ impl<'a, T> OrPoisoned
}
}
impl<'a, T> OrPoisoned
impl<'a, T: ?Sized> OrPoisoned
for Result<RwLockWriteGuard<'a, T>, PoisonError<RwLockWriteGuard<'a, T>>>
{
type Inner = RwLockWriteGuard<'a, T>;
@@ -55,7 +55,7 @@ impl<'a, T> OrPoisoned
}
}
impl<'a, T> OrPoisoned for LockResult<MutexGuard<'a, T>> {
impl<'a, T: ?Sized> OrPoisoned for LockResult<MutexGuard<'a, T>> {
type Inner = MutexGuard<'a, T>;
fn or_poisoned(self) -> Self::Inner {

View File

@@ -8,19 +8,19 @@ codegen-units = 1
lto = true
[dependencies]
leptos = { version = "0.6.13", features = ["csr"] }
leptos_meta = { version = "0.6.13", features = ["csr"] }
leptos_router = { version = "0.6.13", features = ["csr"] }
leptos = { version = "0.7.7", features = ["csr"] }
leptos_meta = { version = "0.7.7" }
leptos_router = { version = "0.7.7" }
console_log = "1.0"
log = "0.4.22"
console_error_panic_hook = "0.1.7"
bevy = "0.14.1"
bevy = "0.15.2"
crossbeam-channel = "0.5.13"
[dev-dependencies]
wasm-bindgen = "0.2.92"
wasm-bindgen-test = "0.3.42"
web-sys = "0.3.69"
wasm-bindgen = "0.2.100"
wasm-bindgen-test = "0.3.50"
web-sys = "0.3.77"
[workspace]
# The empty workspace here is to keep rust-analyzer satisfied

View File

@@ -17,7 +17,7 @@ impl DuplexEventsPlugin {
let (bevy_sender, client_receiver) = crossbeam_channel::bounded(50);
// For sending message from the client to bevy
let (client_sender, bevy_receiver) = crossbeam_channel::bounded(50);
let instance = DuplexEventsPlugin {
DuplexEventsPlugin {
client_processor: EventProcessor {
sender: client_sender,
receiver: client_receiver,
@@ -26,8 +26,7 @@ impl DuplexEventsPlugin {
sender: bevy_sender,
receiver: bevy_receiver,
},
};
instance
}
}
/// Get the client event processor

View File

@@ -23,14 +23,13 @@ impl Scene {
/// Create a new instance
pub fn new(canvas_id: String) -> Scene {
let plugin = DuplexEventsPlugin::new();
let instance = Scene {
Scene {
is_setup: false,
canvas_id: canvas_id,
canvas_id,
evt_plugin: plugin.clone(),
shared_state: SharedState::new(),
processor: plugin.get_processor(),
};
instance
}
}
/// Get the shared state
@@ -47,7 +46,7 @@ impl Scene {
/// Setup and attach the bevy instance to the html canvas element
pub fn setup(&mut self) {
if self.is_setup == true {
if self.is_setup {
return;
};
App::new()
@@ -76,40 +75,37 @@ fn setup_scene(
) {
let name = resource.0.lock().unwrap().name.clone();
// circular base
commands.spawn(PbrBundle {
mesh: meshes.add(Circle::new(4.0)),
material: materials.add(Color::WHITE),
transform: Transform::from_rotation(Quat::from_rotation_x(
commands.spawn((
Mesh3d(meshes.add(Circle::new(4.0))),
MeshMaterial3d(materials.add(Color::WHITE)),
Transform::from_rotation(Quat::from_rotation_x(
-std::f32::consts::FRAC_PI_2,
)),
..default()
});
));
// cube
commands.spawn((
PbrBundle {
mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)),
material: materials.add(Color::rgb_u8(124, 144, 255)),
transform: Transform::from_xyz(0.0, 0.5, 0.0),
..default()
},
Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))),
MeshMaterial3d(materials.add(Color::srgb_u8(124, 144, 255))),
Transform::from_xyz(0.0, 0.5, 0.0),
Cube,
));
// light
commands.spawn(PointLightBundle {
point_light: PointLight {
commands.spawn((
PointLight {
shadows_enabled: true,
..default()
},
transform: Transform::from_xyz(4.0, 8.0, 4.0),
..default()
});
Transform::from_xyz(4.0, 8.0, 4.0),
));
// camera
commands.spawn(Camera3dBundle {
transform: Transform::from_xyz(-2.5, 4.5, 9.0)
.looking_at(Vec3::ZERO, Vec3::Y),
..default()
});
commands.spawn(TextBundle::from_section(name, TextStyle::default()));
commands.spawn((
Camera3d::default(),
Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),
));
commands.spawn((Text::new(name), TextFont::default()));
}
/// Move the Cube on event

View File

@@ -1,6 +1,6 @@
mod demos;
mod routes;
use leptos::*;
use leptos::prelude::*;
use routes::RootPage;
pub fn main() {

View File

@@ -2,7 +2,7 @@ use crate::demos::bevydemo1::eventqueue::events::{
ClientInEvents, CounterEvtData,
};
use crate::demos::bevydemo1::scene::Scene;
use leptos::*;
use leptos::prelude::*;
/// 3d view component
#[component]
@@ -10,18 +10,18 @@ pub fn Demo1() -> impl IntoView {
// Setup a Counter
let initial_value: i32 = 0;
let step: i32 = 1;
let (value, set_value) = create_signal(initial_value);
let (value, set_value) = signal(initial_value);
// Setup a bevy 3d scene
let scene = Scene::new("#bevy".to_string());
let sender = scene.get_processor().sender;
let (sender_sig, _set_sender_sig) = create_signal(sender);
let (scene_sig, _set_scene_sig) = create_signal(scene);
let (sender_sig, _set_sender_sig) = signal(sender);
let (scene_sig, _set_scene_sig) = signal(scene);
// We need to add the 3D view onto the canvas post render.
create_effect(move |_| {
Effect::new(move |_| {
request_animation_frame(move || {
scene_sig.get().setup();
scene_sig.get_untracked().setup();
});
});

View File

@@ -1,9 +1,11 @@
pub mod demo1;
use demo1::Demo1;
use leptos::*;
use leptos_meta::{provide_meta_context, Meta, Stylesheet, Title};
use leptos_router::*;
use leptos::prelude::*;
use leptos_meta::Meta;
use leptos_meta::Title;
use leptos_meta::{provide_meta_context, MetaTags, Stylesheet};
use leptos_router::components::*;
use leptos_router::StaticSegment;
#[component]
pub fn RootPage() -> impl IntoView {
provide_meta_context();
@@ -13,11 +15,12 @@ pub fn RootPage() -> impl IntoView {
<Meta name="description" content="Leptonic CSR template"/>
<Meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<Meta name="theme-color" content="#e66956"/>
<Stylesheet href="https://fonts.googleapis.com/css?family=Roboto&display=swap"/>
<Title text="Leptos Bevy3D Example"/>
<Stylesheet href="https://fonts.googleapis.com/css?family=Roboto&display=swap"/>
<MetaTags/>
<Router>
<Routes>
<Route path="" view=|| view! { <Demo1/> }/>
<Routes fallback=move || "Not found.">
<Route path=StaticSegment("") view=Demo1 />
</Routes>
</Router>
}

View File

@@ -1,6 +1,6 @@
[package]
name = "reactive_graph"
version = "0.1.7"
version = "0.2.0-beta"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
@@ -28,7 +28,7 @@ send_wrapper = { version = "0.6.0", features = ["futures"] }
web-sys = { version = "0.3.72", features = ["console"] }
[dev-dependencies]
tokio = { version = "1.41", features = ["rt-multi-thread", "macros"] }
tokio = { version = "1.43", features = ["rt-multi-thread", "macros"] }
tokio-test = { version = "0.4.4" }
any_spawner = { workspace = true, features = ["futures-executor", "tokio"] }

View File

@@ -961,11 +961,10 @@ where
}
}
impl<I, O, S> Action<I, O, S>
impl<I, O> Action<I, O>
where
I: Send + Sync + 'static,
O: Send + Sync + 'static,
S: Storage<ArcAction<I, O>>,
{
/// Creates a new action, which does not require the action itself to be `Send`, but will run
/// it on the same thread it was created on.
@@ -1006,6 +1005,56 @@ where
}
}
impl<I, O> Action<I, O, LocalStorage>
where
I: 'static,
O: 'static,
{
/// Creates a new action, which neither requires the action itself nor the
/// value it returns to be `Send`. If this action is accessed from outside the
/// thread on which it was created, it panics.
///
/// This combines the features of [`Action::new_local`] and [`Action::new_unsync`].
#[track_caller]
pub fn new_unsync_local<F, Fu>(action_fn: F) -> Self
where
F: Fn(&I) -> Fu + 'static,
Fu: Future<Output = O> + 'static,
{
Self {
inner: ArenaItem::new_with_storage(ArcAction::new_unsync(
action_fn,
)),
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
}
}
/// Creates a new action, which neither requires the action itself nor the
/// value it returns to be `Send`, and provides it with an initial value.
/// If this action is accessed from outside the thread on which it was created, it panics.
///
/// This combines the features of [`Action::new_local_with_value`] and
/// [`Action::new_unsync_with_value`].
#[track_caller]
pub fn new_unsync_local_with_value<F, Fu>(
value: Option<O>,
action_fn: F,
) -> Self
where
F: Fn(&I) -> Fu + 'static,
Fu: Future<Output = O> + 'static,
{
Self {
inner: ArenaItem::new_with_storage(
ArcAction::new_unsync_with_value(value, action_fn),
),
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
}
}
}
impl<I, O, S> DefinedAt for Action<I, O, S> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(any(debug_assertions, leptos_debuginfo))]

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