Compare commits

...

179 Commits

Author SHA1 Message Date
Greg Johnston
0798e0812d fix: unused import in example 2024-10-01 21:23:22 -04:00
Greg Johnston
03514e68be chore: fix test import 2024-10-01 20:43:46 -04:00
Greg Johnston
4092368581 chore: remove unused import 2024-10-01 20:43:37 -04:00
Greg Johnston
896f7de8e1 fix: update import of spawn_local in test examples 2024-10-01 20:06:04 -04:00
Greg Johnston
29b2d3024a fix: update import of tick in test examples 2024-10-01 20:05:22 -04:00
Greg Johnston
0d4f3b51e9 fix: import of reactive_graph in integrations 2024-10-01 19:38:55 -04:00
Greg Johnston
2431b19cdf chore: reexport reactive_graph as leptos::reactive 2024-10-01 19:36:20 -04:00
Greg Johnston
4801e1ec6d chore: hide unwrap_signal from docs 2024-10-01 19:36:20 -04:00
Greg Johnston
e206b93ba5 chore: inline docs for reexported crates 2024-10-01 19:36:20 -04:00
Greg Johnston
f0675446d8 chore: reexport reactive_graph as leptos::reactive 2024-10-01 19:36:20 -04:00
Greg Johnston
2ac7eaad15 chore: add missing docs for tick 2024-10-01 19:36:20 -04:00
Greg Johnston
8a040fda69 chore: rename Writeable to Write for consistency 2024-10-01 19:36:20 -04:00
Greg Johnston
2f5c966cf4 chore: reexport untrack from leptos::prelude but not from reactive_graph 2024-10-01 19:36:20 -04:00
Greg Johnston
45e2629f0e chore: remove reexport of tachys in leptos::prelude 2024-10-01 19:36:20 -04:00
Greg Johnston
517566d085 change: rename spawn module to task 2024-10-01 19:35:45 -04:00
Greg Johnston
6df8657700 fix: escape text nodes and attributes in InertHtml (closes #3056) (#3057) 2024-10-01 19:19:21 -04:00
Georg Vienna
2a4063a259 projects: port session_auth_axum (#2970) 2024-10-01 09:40:57 -07:00
Marc-Stefan Cassola
013ec4a09d Two-way data binding (#2977)
* added two-way data binding to dom elements

* added two-way data binding to radio groups

* moved bind into reactive_graph mod

* chore: use `::leptos` absolute path in macro

* chore: move `Group` into the reactive module, as that's the only place it has meaning

* feat: use new `bind:` syntax

* chore: update for non-generic rendering

* chore: ignore this test

---------

Co-authored-by: Greg Johnston <greg.johnston@gmail.com>
2024-10-01 09:39:45 -07:00
Greg Johnston
d10fec08e2 Merge pull request #3037 from leptos-rs/rename-island-router
change: rename island-router feature so people don't use it
2024-09-30 20:55:08 -04:00
Greg Johnston
94f4328586 example: add README that actually explains this example 2024-09-30 20:08:39 -04:00
Greg Johnston
2b70961110 change: rename island-router feature so people don't use it 2024-09-30 20:08:24 -04:00
Greg Johnston
699c54e16c -gamma prerelease 2024-09-30 14:16:38 -04:00
Greg Johnston
1217ef4d8e beta7 2024-09-30 14:16:01 -04:00
Sean Aye
9aba3efbe4 chore: remove unnecessary trait bounds (#3046) 2024-09-29 17:26:31 -04:00
SleeplessOne1917
1b48a2f8d5 Replace proc-macro-error with proc-macro-error2 (#3044) 2024-09-28 18:03:28 -07:00
Greg Johnston
31cb766206 docs: restore warning when reading resource outside Suspense (closes #3038) (#3041) 2024-09-28 13:10:30 -04:00
Greg Johnston
de28a317f0 fix: do not emit HTML comments inside script/style, and do not try to hydrate them (closes #3033) (#3034) 2024-09-27 12:33:51 -04:00
Greg Johnston
d29433b98d fix: more robust arena sandboxing (closes #2995) (#3026) 2024-09-26 19:48:04 -04:00
Greg Johnston
f2582b6ac9 fix: properly scope Suspend rebuilds within ownership tree (closes #3024) (#3030) 2024-09-26 18:19:36 -04:00
zakstucke
67845be161 feat: read and write for StoredValue (#3027) 2024-09-26 15:46:09 -04:00
Greg Johnston
2bf1f46b88 fix: add correct namespaces for SVG and MathML elements (closes #3021) (#3025) 2024-09-25 19:42:42 -04:00
VincentBerthier
70ae3a0abb fix: island hydration warning message (#3023) 2024-09-25 15:38:04 -04:00
Greg Johnston
96e2b5cba1 fix: abort Suspend on cleanup, to avoid using disposed items (closes #3012) (#3019) 2024-09-24 21:45:39 -04:00
Greg Johnston
ef27a198d9 fix: correct Owner restoration in FlatRoutes (closes #3014) (#3018) 2024-09-24 21:45:30 -04:00
Greg Johnston
47299926bb fix: suppress resource loading during route generation (closes #3013) (#3017) 2024-09-24 21:45:22 -04:00
Greg Johnston
bdc2285658 change: remove generic rendering (#3015) 2024-09-24 17:01:57 -04:00
zakstucke
9d4ce6e526 Fix custom root when not using islands. (#3016) 2024-09-24 11:42:54 -07:00
Greg Johnston
3d2cdc21a1 beta6 2024-09-23 08:57:44 -04:00
Greg Johnston
93d939aef8 Merge pull request #3009 from leptos-rs/stores-patch
Stores patching
2024-09-22 21:45:10 -04:00
Greg Johnston
fb04750607 example: add example of patching fields 2024-09-22 19:52:48 -04:00
Greg Johnston
a080496e7e fix: notify correctly when patching stores 2024-09-22 19:51:50 -04:00
Greg Johnston
9fc1002167 perf: do not notify keyed iterator when only an inner field has changed 2024-09-22 19:50:30 -04:00
Greg Johnston
bc5c766530 fix: notify stores when modifying parent, without notifying siblings 2024-09-22 19:33:14 -04:00
Greg Johnston
17821f863a fix: restructure StoredValue so that nested calls do not deadlock (closes #2968) (#3004) 2024-09-22 19:21:49 -04:00
Baptiste
1ca4f34ef3 fix: optional props with islands (#3005) 2024-09-21 18:13:58 -04:00
Greg Johnston
8f0a1554b1 Merge pull request #3000 from leptos-rs/add-attr-any-view
Doing chores
2024-09-21 08:51:32 -04:00
Greg Johnston
38d4f26d03 fix: snip infinite type recursion 2024-09-21 07:51:27 -04:00
Greg Johnston
2b04c2710d Merge pull request #3002 from leptos-rs/fix-stores
Fix stores
2024-09-21 07:37:54 -04:00
Greg Johnston
a4937a1236 fix: correctly triggering parents vs siblings 2024-09-20 21:25:42 -04:00
Greg Johnston
f6f2c39686 chore: remove UntrackedWriter 2024-09-20 20:47:46 -04:00
Greg Johnston
d7eacf1ab5 chore: remove unused DocumentFragment mounting code 2024-09-20 20:41:51 -04:00
Greg Johnston
d1a4bbe28e chore: fix adding attr todo for static types 2024-09-20 20:41:51 -04:00
Greg Johnston
412ecd6b1b chore: remove dead iterator wrapper code 2024-09-20 20:41:51 -04:00
Greg Johnston
9bc0152121 chore: clean up todos in template SSR code 2024-09-20 20:41:51 -04:00
Greg Johnston
4b05cada8f chore: remove dead code for rendering guards 2024-09-20 20:41:51 -04:00
Greg Johnston
a818862704 chore: tidy up todos on EitherKeepAlive 2024-09-20 20:41:51 -04:00
Greg Johnston
173487debc fix: add add_any_attr() for AnyView<R> 2024-09-20 20:41:51 -04:00
Greg Johnston
449d96cc9a feat: add wrapper to support add_any_attr() fixes 2024-09-20 20:41:51 -04:00
Greg Johnston
f9bf6a95ed Merge pull request #3001 from leptos-rs/ci
Fix CI
2024-09-20 20:40:53 -04:00
Greg Johnston
5bf6c94bb2 chore: remove some testing combinations 2024-09-20 20:11:44 -04:00
Greg Johnston
e1ce94a28d fix: rationalize reactive_stores tests 2024-09-20 18:25:40 -04:00
Greg Johnston
2a62dcf27e chore: fix unused import 2024-09-20 18:18:11 -04:00
Greg Johnston
3094766c5c chore: allow unused methods 2024-09-20 18:08:09 -04:00
Greg Johnston
a52804595d chore: allow unused methods 2024-09-20 17:35:43 -04:00
Greg Johnston
e72f12d32b chore: clean up tests 2024-09-20 17:31:11 -04:00
Greg Johnston
e70083708a chore: fix dependencies 2024-09-20 16:39:21 -04:00
Greg Johnston
cbc4caef19 chore: fix testing 2024-09-20 16:36:40 -04:00
Greg Johnston
fbeee4dbf5 chore: cargo fmt 2024-09-20 16:31:07 -04:00
Greg Johnston
d13f7e5438 chore: remove unused import 2024-09-20 16:02:41 -04:00
Greg Johnston
7b543bd31c chore: fix syn features 2024-09-20 15:57:54 -04:00
Greg Johnston
1743724420 chore: AsRef/AsMut 2024-09-20 15:46:15 -04:00
Greg Johnston
73e0add670 chore: cargo fmt 2024-09-20 15:44:19 -04:00
Greg Johnston
4f5eb444bc chore: remove print 2024-09-20 15:43:46 -04:00
Greg Johnston
7de98823fb chore: clippy and missing GTK implementations 2024-09-20 15:38:15 -04:00
Georg Vienna
6d930573fc feat: add either macro (#2900) 2024-09-20 08:17:40 -07:00
Saber Haj Rabiee
3317002ff5 fix: improve CI workflow (#2838)
* fix: improve CI workflow

* fix: add missing `Makefile.toml` to workspace crates
(reactive_stores and reactive_stores_macro)

* fix: remove trailing slash in workflow names

* fix: add gtk example and improve excluding example's logic

* fix: install gtk example deps
2024-09-20 08:08:28 -07:00
martin frances
99403d0167 MSRV Bump to 1.80.0, drop crate lazy_static and use std::sync::LazyLock. (#2767) 2024-09-19 21:40:33 -07:00
Greg Johnston
23ce022c60 Merge pull request #2997 from leptos-rs/2971
Changes related to stack overflows
2024-09-19 17:20:45 -04:00
Greg Johnston
96e1fd0fb8 fix: turn off InertHtml for SVG/MathML (closes #2998) (#2999) 2024-09-19 17:20:30 -04:00
Greg Johnston
f28dac1093 chore: remove unused debug info from HtmlElement 2024-09-19 15:38:52 -04:00
Greg Johnston
ff28544fb2 feat: add BoxedView wrapper 2024-09-19 15:38:52 -04:00
Deep Gaurav
27765b417c fix: static generation should wait for file writes before resolving (#2994) 2024-09-19 15:36:54 -04:00
Greg Johnston
b0d8d4ee26 fix: properly trigger Suspense when Suspend is called again (#2993) 2024-09-18 21:35:37 -04:00
Greg Johnston
c4b1176a6a fix: drop lock on current URL before rendering, in case of redirect (closes #2990) (#2992) 2024-09-18 21:27:18 -04:00
Greg Johnston
fd133dd79a chore: re-export untrack 2024-09-18 20:25:35 -04:00
Greg Johnston
9c2477a4cf Revert "chore: re-export untrack (#2991)"
This reverts commit f3b6d1f351.
2024-09-18 20:25:03 -04:00
Greg Johnston
f3b6d1f351 chore: re-export untrack (#2991) 2024-09-18 19:51:20 -04:00
Greg Johnston
5af7b54c9c perf: optimize inert HTML elements (#2989) 2024-09-18 19:42:07 -04:00
Baptiste
ba9604101d fix: correct lifetimes for PartialPathMatch (#2981) 2024-09-16 21:59:49 -04:00
Greg Johnston
e136c1fc44 Merge pull request #2986 from leptos-rs/2957
fix: get Suspend and ErrorBoundary working together correctly
2024-09-16 21:58:49 -04:00
Greg Johnston
c581b3293e Merge pull request #2985 from leptos-rs/2982
fix: sort attributes so `class` and `style` always run before `class:` and `style:` (closes #2982)
2024-09-16 21:58:38 -04:00
Greg Johnston
cc7f861637 fix: add missing Debug and DefinedAt impls for Resource/ArcResource (closes #2983) (#2984) 2024-09-16 21:58:23 -04:00
Greg Johnston
642d6fc72b fix: stable hashes across different client and server builds for islands (closes #2978) (#2980) 2024-09-16 21:58:09 -04:00
Kajetan Welc
e69c7f4ae0 feat(#2946): add #[store(skip)] for #[derive(Store)] (#2975) 2024-09-16 21:57:57 -04:00
Greg Johnston
9ca36d4763 chore: remove deprecated function in doctest example 2024-09-16 21:34:04 -04:00
Greg Johnston
8dc600ca02 fix: do not sort class and style that are after spread marker 2024-09-16 21:33:46 -04:00
Greg Johnston
b621ead607 fix: forward subscribers for already-resolved Suspend during client-side build 2024-09-16 21:10:02 -04:00
Greg Johnston
66cf21f650 fix: Ok => Err transition with Suspend (closes #2957) 2024-09-16 21:02:30 -04:00
Greg Johnston
f3dcdc057d fix: sort attributes so class and style always run before class: and style: (closes #2982) 2024-09-16 20:26:51 -04:00
Greg Johnston
2bdacf636e Merge pull request #2974 from leptos-rs/more-stores
Improve efficiency of keyed stores
2024-09-14 21:49:27 -04:00
Greg Johnston
fc06980c60 feat: directly implement IntoIterator on keyed fields 2024-09-14 21:22:16 -04:00
Greg Johnston
550a3a4e6d perf: use FxHashMap for sets of keys 2024-09-14 21:20:09 -04:00
Greg Johnston
3310e7766b perf: only rebuild keys when we've touched the list itself, not when we edit a row 2024-09-14 18:33:59 -04:00
Greg Johnston
5ab865e89d example: fix so we remove by ID, not by id-as-index 2024-09-14 18:33:36 -04:00
Greg Johnston
f0c60f6ef6 Merge pull request #2871 from leptos-rs/more-stores
(draft) More work on stores
2024-09-14 17:34:13 -04:00
Greg Johnston
f3f685c923 chore: clean up Trigger <> Notify rename 2024-09-14 17:00:47 -04:00
Greg Johnston
3646bf31b0 example: only show date if in scheduling mode 2024-09-14 17:00:31 -04:00
Greg Johnston
b39895fa2d feat: remove outdated entries and recycle their keys 2024-09-14 17:00:11 -04:00
Greg Johnston
1fce8931ab fix: do not match incomplete static segments (closes #2916) (#2973) 2024-09-14 16:20:45 -04:00
Greg Johnston
6166f6edbd fix: ensure that we retain the correct sandoxed arena when spawning Futures (#2965) 2024-09-14 09:10:01 -04:00
Greg Johnston
dc9fbb0585 Merge branch 'main' into more-stores 2024-09-13 17:31:54 -04:00
Greg Johnston
d7b2f9d05b feat: keyed store fields, with keyed iteration 2024-09-13 17:21:09 -04:00
Greg Johnston
69c4090d32 example: update stores example to show enum matching 2024-09-13 12:12:05 -04:00
Greg Johnston
fff5fa3459 feat: add support for named and unnamed enum fields in Stores 2024-09-13 11:11:31 -04:00
Greg Johnston
e92b80c71e fix: tracking ArcStore should track, not trigger, its root trigger 2024-09-13 10:54:07 -04:00
Greg Johnston
8bb04ef248 chore: remove dead code 2024-09-13 10:53:27 -04:00
luoxiaozero
d7881ccfb5 fix: allow component to use span prop when tracing feature is enabled (#2969) 2024-09-13 09:35:59 -04:00
Álvaro Mondéjar Rubio
96a1f80daf tests: fix Effect doctests not being executed, and test-related issues (#2886) 2024-09-12 16:43:32 -04:00
Greg Johnston
a083b57260 fix: do not panic in automatic Track implementation if source is disposed (#2964) 2024-09-12 09:22:46 -04:00
Chris
4fa6660a3f doc: router::{Wildcard, Static, Param}Segment (#2949) 2024-09-11 21:02:02 -04:00
Baptiste
43f2ad7043 chore: add #[track_caller] to to_any_source (#2963) 2024-09-11 20:01:06 -04:00
Greg Johnston
2bf04072ea Merge pull request #2959 from leptos-rs/2956
fix: do not retrigger parent effect when Suspend's resources resolve (closes #2956)
2024-09-10 14:08:32 -04:00
Greg Johnston
efc6fc017d fix: forward subscribers for already-resolved Suspend during hydration 2024-09-10 06:59:47 -04:00
Greg Johnston
6cb10401df chore(ci): update list of core crates 2024-09-09 21:20:14 -04:00
Greg Johnston
346efd66f5 chore: remove unused cancellation logic for now 2024-09-09 21:19:55 -04:00
Greg Johnston
7c0889e873 fix: do not retrigger parent effect when Suspend's resources resolve (closes #2956) 2024-09-09 18:10:48 -04:00
Greg Johnston
bb40576bd5 Merge pull request #2955 from leptos-rs/fix-refetch
Fix refetch
2024-09-09 07:33:01 -04:00
Greg Johnston
6baf20275f fix: Resource::refetch() 2024-09-08 21:41:44 -04:00
Greg Johnston
5a57d48913 beta5 2024-09-08 19:40:32 -04:00
jk
73f0207a7d feat: add a copyable Trigger type (closes #2901) (#2939) 2024-09-08 19:39:40 -04:00
Greg Johnston
4e4fb8ab10 chore(examples): SsrMode is no longer clone (#2954) 2024-09-08 19:39:15 -04:00
Greg Johnston
b9cccc6b91 fix: revert change, making writes to children notify parents 2024-09-08 13:58:45 -04:00
Greg Johnston
d42163d888 fix: tracking a subfield should track its parents, as changing these can change it 2024-09-08 13:56:32 -04:00
Greg Johnston
2db3e4f4d8 chore: move Option tests into option module 2024-09-08 13:56:32 -04:00
Greg Johnston
45380a258a feat: add support for simple enums in stores 2024-09-08 13:56:32 -04:00
Greg Johnston
40292d0896 feat: add mapping over Option store fields 2024-09-08 13:56:32 -04:00
Greg Johnston
e8be9e31ff fix/change: do not trigger every parent when writing to a store subfield 2024-09-08 13:56:32 -04:00
Greg Johnston
3d0fdb1ab0 feat: add mapped unwrap for Option store fields 2024-09-08 13:56:32 -04:00
Tommy Yu
4dea1195e2 examples: include axum_js_ssr for discussion of integrating JS libraries (#2878) 2024-09-08 13:42:14 -04:00
Greg Johnston
92ea39ddac feat: version Resources to avoid race conditions (#2950) 2024-09-08 13:27:55 -04:00
Matt Kane
05e08166c4 chore: update suspense_test example to use Resource::map (#2877) 2024-09-07 20:56:30 -04:00
Greg Johnston
827cc0bdfa fix: ensure Resource always tracks its source, and does not double-run (#2948) 2024-09-07 18:57:31 -04:00
Greg Johnston
57bd343f4a fix: remove Owner from thread-local when it is ordinarily dropped, to ensure cleanup (closes #2942) (#2944) 2024-09-07 07:01:54 -04:00
Azriel Hoh
4a76aead68 Switch to proc-macro-error2 to address unmaintained security advisory. (#2934) 2024-09-06 17:24:03 -04:00
Greg Johnston
48c2148589 fix: untrack in the async block of a Resource (closes #2937) (#2941) 2024-09-06 17:23:40 -04:00
Greg Johnston
32bea69c28 fix: implement dry_resolve on Suspend so that resources created inside a Suspend are registered (closes #2917) (#2940) 2024-09-06 14:49:37 -04:00
Greg Johnston
f3c57f8bce Merge pull request #2929 from leptos-rs/relative-flat
feat: correctly support relative routing for `FlatRoutes`
2024-09-05 09:13:07 -04:00
Greg Johnston
000896b2f7 fix: allow clone: syntax on components (closes #2903) (#2928) 2024-09-04 20:44:37 -04:00
Greg Johnston
88004e5042 feat: correctly support relative routing for FlatRoutes 2024-09-04 20:43:59 -04:00
Greg Johnston
6001a93475 fix: correctly set ownership on view of ProtectedParentRoute (closes #2897) (#2927)
* fix: correctly set ownership on view of ProtectedParentRoute (closes #2897)

* chore: clippy false positive
2024-09-04 20:42:10 -04:00
Greg Johnston
4784b2ddab chore: remove dead code 2024-09-04 20:20:49 -04:00
Chris Biscardi
32b4cd008f Enable CDN support for assets (#2925)
* Enable CDN support for assets

This enables setting a root for a url, especially relating to generated .wasm, .js, and .css files.

This allows using a CDN for static assets.

* fix lint
2024-09-04 15:05:51 -07:00
Greg Johnston
823f8b51be Merge pull request #2924 from leptos-rs/2915 2024-09-04 11:15:11 -04:00
Greg Johnston
209743d6bc fix: ensure unique names for islands (closes #2915) 2024-09-04 08:53:03 -04:00
Greg Johnston
b93a88accc Merge pull request #2921 from leptos-rs/2920 2024-09-03 21:03:29 -04:00
Greg Johnston
dc2314d5e2 fix: ensure that LocalResource never creates a SendWrapper on the server 2024-09-03 20:40:12 -04:00
Greg Johnston
33aa676854 fix: ensure stability of hydration IDs on client and server, when using partial hydration (closes #2920) 2024-09-03 19:53:21 -04:00
Greg Johnston
4a3b3ffb8a fix: allow > 26 children for element children as well as top-level fragments (#2918) 2024-09-03 14:53:38 -04:00
Greg Johnston
ee5cbf1891 fix/change: remove replace and state props from <A/>, and add them via spreading instead (#2912) 2024-09-03 14:25:25 -04:00
mrvillage
8fcf3544a8 fix: trim spaces in file hashes (closes #2908) (#2913) 2024-09-03 08:51:03 -04:00
Greg Johnston
2b8e987cb8 fix: check whether we're on the server before adding window event listener (closes #2891) (#2910) 2024-09-02 09:11:07 -04:00
Greg Johnston
998165148b fix: automatically break children into tuples of a size the trait system can handle (closes #2863) (#2909) 2024-09-02 08:01:41 -04:00
Greg Johnston
c80eff1098 Merge pull request #2898 from leptos-rs/min-serialization
fix: only serialize Resources if we're in a part of the tree that needs hydration
2024-09-02 07:11:12 -04:00
Greg Johnston
cd8f2c2153 fix: correctly provide parent outlets when rebuilding from fewer to more nested routes (closes #2904) (#2906) 2024-09-02 07:08:15 -04:00
Greg Johnston
cb0abff2d5 fix: only serialize resource data if we're in a part of the tree being hydrated (i.e., in full hydration or in an island) 2024-08-31 16:18:06 -04:00
Greg Johnston
3b1b2e2dcc fix version for publish 2024-08-31 11:59:05 -04:00
Greg Johnston
7831e4ad05 next beta release 2024-08-31 11:59:05 -04:00
Greg Johnston
e7bb859cd9 feat: add support for static routing and incremental static regeneration (#2875) 2024-08-31 10:33:12 -04:00
Rakshith Ravi
9fc26e609c feat: allow for documentation and other attributes to fields in server fn (#2876) 2024-08-31 09:35:08 -04:00
mrvillage
4f1ee65e6c Add hash files support to 0.7 (#2894)
* Add support for JS and WASM file name hashing

* Add `<HashedStylesheet />`

* Update `<HashedStylesheet />`

* Whoops

* Fix formatting

* My IDE is just refusing to work apparently

* I hate my IDE

* Don't run the doctest

* Just remove the example, I don't know enough about doctest for this
2024-08-30 14:29:59 -07:00
Greg Johnston
ceff827a77 Merge pull request #2884 from leptos-rs/rstml-0.12
update to rstml and improve recoverability in attributes
2024-08-28 07:46:08 -04:00
Álvaro Mondéjar Rubio
a7db918775 docs: update main documentation of leptos crate (#2853) 2024-08-28 07:44:29 -04:00
Baptiste
be20ecd366 fix: implement Copy and Clone for HtmlElement without needing Rndr to be Clone/copy (#2889) 2024-08-28 07:25:51 -04:00
Greg Johnston
5790d8ad12 fix: derive various traits on Dom to make it easier to derive traits on structs that take a generic Renderer 2024-08-28 07:25:08 -04:00
luoxiaozero
7dc58e248c fix: compile attr:aria-* syntax (#2887) 2024-08-27 09:00:13 -07:00
Greg Johnston
7b03e63b23 docs: add note about curly braces 2024-08-26 21:00:45 -04:00
Greg Johnston
55fd7c6421 chore: clippy 2024-08-26 20:57:02 -04:00
Greg Johnston
ba1ea4c2bb do not error on unbraced 2024-08-26 20:56:23 -04:00
blorbb
6a4fc96835 rstml 0.12 and enforce braces in attribute values 2024-08-26 20:55:12 -04:00
267 changed files with 15379 additions and 7153 deletions

View File

@@ -1,23 +1,21 @@
name: CI Changed Examples
on:
push:
branches:
- main
- leptos_0.6
pull_request:
branches:
- main
- leptos_0.6
jobs:
get-example-changed:
uses: ./.github/workflows/get-example-changed.yml
get-matrix:
needs: [get-example-changed]
uses: ./.github/workflows/get-changed-examples-matrix.yml
with:
example_changed: ${{ fromJSON(needs.get-example-changed.outputs.example_changed) }}
test:
name: CI
needs: [get-example-changed, get-matrix]

View File

@@ -1,13 +1,13 @@
name: CI Examples
on:
push:
branches:
- main
- leptos_0.6
pull_request:
branches:
- main
- leptos_0.6
jobs:
get-leptos-changed:
uses: ./.github/workflows/get-leptos-changed.yml

View File

@@ -1,27 +1,24 @@
name: CI semver
on:
push:
branches:
- main
- leptos_0.6
pull_request:
branches:
- main
- leptos_0.6
jobs:
get-leptos-changed:
uses: ./.github/workflows/get-leptos-changed.yml
test:
needs: [get-leptos-changed]
if: github.event.pull_request.labels[0].name == 'semver' # needs.get-leptos-changed.outputs.leptos_changed == 'true' && github.event.pull_request.labels[0].name != 'breaking'
name: Run semver check (nightly-2024-08-01)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Semver Checks
uses: obi1kenobi/cargo-semver-checks-action@v2
with:

View File

@@ -1,50 +1,25 @@
name: CI
on:
push:
branches:
- main
- leptos_0.6
pull_request:
branches:
- main
- leptos_0.6
jobs:
get-leptos-changed:
uses: ./.github/workflows/get-leptos-changed.yml
get-leptos-matrix:
uses: ./.github/workflows/get-leptos-matrix.yml
test:
name: CI
needs: [get-leptos-changed]
needs: [get-leptos-changed, get-leptos-matrix]
if: needs.get-leptos-changed.outputs.leptos_changed == 'true'
strategy:
matrix:
directory:
[
any_error,
any_spawner,
const_str_slice_concat,
either_of,
hydration_context,
integrations/actix,
integrations/axum,
integrations/utils,
leptos,
leptos_config,
leptos_dom,
leptos_hot_reload,
leptos_macro,
leptos_server,
meta,
next_tuple,
oco,
or_poisoned,
reactive_graph,
router,
router_macro,
server_fn,
server_fn/server_fn_macro_default,
server_fn_macro,
]
matrix: ${{ fromJSON(needs.get-leptos-matrix.outputs.matrix) }}
fail-fast: false
uses: ./.github/workflows/run-cargo-make-task.yml
with:
directory: ${{ matrix.directory }}

View File

@@ -1,12 +1,10 @@
name: Examples Changed Call
on:
workflow_call:
outputs:
example_changed:
description: "Example Changed"
value: ${{ jobs.get-example-changed.outputs.example_changed }}
jobs:
get-example-changed:
name: Get Example Changed
@@ -18,7 +16,6 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get example files that changed
id: changed-files
uses: tj-actions/changed-files@v45
@@ -26,13 +23,10 @@ jobs:
files: |
examples/**
!examples/cargo-make/**
!examples/gtk/**
!examples/Makefile.toml
!examples/*.md
- name: List example files that changed
run: echo '${{ steps.changed-files.outputs.all_changed_files }}'
- name: Set example_changed
id: set-example-changed
run: |

View File

@@ -1,38 +1,34 @@
name: Get Examples Matrix Call
on:
workflow_call:
outputs:
matrix:
description: "Matrix"
value: ${{ jobs.create.outputs.matrix }}
jobs:
create:
name: Create Examples Matrix
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
env:
# separate examples using "|" (vertical bar) char like "a|b|c".
# cargo-make should be excluded by default.
EXCLUDED_EXAMPLES: cargo-make
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install jq
run: sudo apt-get install jq
- name: Set Matrix
id: set-matrix
run: |
examples=$(ls examples |
awk '{print "examples/" $0}' |
grep -v .md |
grep -v examples/Makefile.toml |
grep -v examples/cargo-make |
grep -v examples/gtk |
examples=$(ls -1d examples/*/ |
grep -vE "($EXCLUDED_EXAMPLES)" |
sed 's/\/$//' |
jq -R -s -c 'split("\n")[:-1]')
echo "Example Directories: $examples"
echo "matrix={\"directory\":$examples}" >> "$GITHUB_OUTPUT"
- name: Print Location Info
run: |
echo "Workspace: ${{ github.workspace }}"

View File

@@ -1,12 +1,10 @@
name: Get Leptos Changed Call
on:
workflow_call:
outputs:
leptos_changed:
description: "Leptos Changed"
value: ${{ jobs.create.outputs.leptos_changed }}
jobs:
create:
name: Detect Source Change
@@ -18,40 +16,19 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get source files that changed
id: changed-source
uses: tj-actions/changed-files@v45
with:
files: |
any_error/**
any_spawner/**
const_str_slice_concat/**
either_of/**
hydration_context/**
integrations/actix/**
integrations/axum/**
integrations/utils/**
leptos/**
leptos_config/**
leptos_dom/**
leptos_hot_reload/**
leptos_macro/**
leptos_server/**
meta/**
next_tuple/**
oco/**
or_poisoned/**
reactive_graph/**
router/**
router_macro/**
server_fn/**
server_fn/server_fn_macro_default/**
server_fn_macro/**
files_ignore: |
.*/**/*
cargo-make/**/*
examples/**/*
projects/**/*
benchmarks/**/*
docs/**/*
- name: List source files that changed
run: echo '${{ steps.changed-source.outputs.all_changed_files }}'
- name: Set leptos_changed
id: set-source-changed
run: |

32
.github/workflows/get-leptos-matrix.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: Get Leptos Matrix Call
on:
workflow_call:
outputs:
matrix:
description: "Matrix"
value: ${{ jobs.create.outputs.matrix }}
jobs:
create:
name: Create Leptos Matrix
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install jq
run: sudo apt-get install jq
- name: Set Matrix
id: set-matrix
run: |
crates=$(cargo metadata --no-deps --quiet --format-version 1 |
jq -r '.packages[] | select(.name != "workspace") | .manifest_path| rtrimstr("/Cargo.toml")' |
sed "s|$(pwd)/||" |
jq -R -s -c 'split("\n")[:-1]')
echo "Leptos Directories: $crates"
echo "matrix={\"directory\":$crates}" >> "$GITHUB_OUTPUT"
- name: Print Location Info
run: |
echo "Workspace: ${{ github.workspace }}"
pwd
ls | sort -u

View File

@@ -1,5 +1,4 @@
name: Run Task
on:
workflow_call:
inputs:
@@ -12,70 +11,53 @@ on:
toolchain:
required: true
type: string
env:
CARGO_TERM_COLOR: always
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
jobs:
test:
name: Run ${{ inputs.cargo_make_task }} (${{ inputs.toolchain }})
runs-on: ubuntu-latest
steps:
# Setup environment
- uses: actions/checkout@v4
- name: Setup Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ inputs.toolchain }}
- name: Add wasm32-unknown-unknown
run: rustup target add wasm32-unknown-unknown
- name: Setup cargo-make
uses: davidB/rust-cargo-make@v1
- name: Cargo generate-lockfile
run: cargo generate-lockfile
- uses: Swatinem/rust-cache@v2
- name: Install binstall
uses: cargo-bins/cargo-binstall@main
- name: Install wasm-bindgen
run: cargo binstall wasm-bindgen-cli --no-confirm
- name: Install cargo-leptos
run: cargo binstall cargo-leptos --no-confirm
- name: Install Trunk
uses: jetli/trunk-action@v0.5.0
with:
version: "latest"
- name: Print Trunk Version
run: trunk --version
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- uses: pnpm/action-setup@v4
name: Install pnpm
id: pnpm-install
with:
version: 8
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
name: Setup pnpm cache
with:
@@ -83,7 +65,6 @@ jobs:
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Maybe install chromedriver
run: |
project_makefile=${{inputs.directory}}/Makefile.toml
@@ -99,7 +80,6 @@ jobs:
else
echo chromedriver is not required
fi
- name: Maybe install playwright browser dependencies
run: |
for pw_path in $(find ${{inputs.directory}} -name playwright.config.ts)
@@ -113,12 +93,16 @@ jobs:
echo Playwright is not required
fi
done
- name: Install Deno
uses: denoland/setup-deno@v1
with:
deno-version: v1.x
- name: Maybe install gtk-rs dependencies
run: |
if [ ! -z $(echo ${{inputs.directory}} | grep gtk) ]; then
sudo apt-get update
sudo apt-get install -y libglib2.0-dev libgio2.0-cil-dev libgraphene-1.0-dev libcairo2-dev libpango1.0-dev libgtk-4-dev
fi
# Run Cargo Make Task
- name: ${{ inputs.cargo_make_task }}
run: |

View File

@@ -40,36 +40,36 @@ members = [
exclude = ["benchmarks", "examples", "projects"]
[workspace.package]
version = "0.7.0-beta2"
version = "0.7.0-gamma"
edition = "2021"
rust-version = "1.76"
[workspace.dependencies]
throw_error = { path = "./any_error/", version = "0.2.0-beta2" }
throw_error = { path = "./any_error/", version = "0.2.0-gamma" }
any_spawner = { path = "./any_spawner/", version = "0.1.0" }
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1.0" }
either_of = { path = "./either_of/", version = "0.1.0" }
hydration_context = { path = "./hydration_context", version = "0.2.0-beta2" }
leptos = { path = "./leptos", version = "0.7.0-beta2" }
leptos_config = { path = "./leptos_config", version = "0.7.0-beta2" }
leptos_dom = { path = "./leptos_dom", version = "0.7.0-beta2" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.0-beta2" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.0-beta2" }
leptos_macro = { path = "./leptos_macro", version = "0.7.0-beta2" }
leptos_router = { path = "./router", version = "0.7.0-beta2" }
leptos_router_macro = { path = "./router_macro", version = "0.7.0-beta2" }
leptos_server = { path = "./leptos_server", version = "0.7.0-beta2" }
leptos_meta = { path = "./meta", version = "0.7.0-beta2" }
next_tuple = { path = "./next_tuple", version = "0.1.0-beta2" }
hydration_context = { path = "./hydration_context", version = "0.2.0-gamma" }
leptos = { path = "./leptos", version = "0.7.0-gamma" }
leptos_config = { path = "./leptos_config", version = "0.7.0-gamma" }
leptos_dom = { path = "./leptos_dom", version = "0.7.0-gamma" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.0-gamma" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.0-gamma" }
leptos_macro = { path = "./leptos_macro", version = "0.7.0-gamma" }
leptos_router = { path = "./router", version = "0.7.0-gamma" }
leptos_router_macro = { path = "./router_macro", version = "0.7.0-gamma" }
leptos_server = { path = "./leptos_server", version = "0.7.0-gamma" }
leptos_meta = { path = "./meta", version = "0.7.0-gamma" }
next_tuple = { path = "./next_tuple", version = "0.1.0-gamma" }
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.0-beta2" }
reactive_stores = { path = "./reactive_stores", version = "0.1.0-beta2" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0-beta2" }
server_fn = { path = "./server_fn", version = "0.7.0-beta2" }
server_fn_macro = { path = "./server_fn_macro", version = "0.7.0-beta2" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.0-beta2" }
tachys = { path = "./tachys", version = "0.1.0-beta2" }
reactive_graph = { path = "./reactive_graph", version = "0.1.0-gamma" }
reactive_stores = { path = "./reactive_stores", version = "0.1.0-gamma" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0-gamma" }
server_fn = { path = "./server_fn", version = "0.7.0-gamma" }
server_fn_macro = { path = "./server_fn_macro", version = "0.7.0-gamma" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.0-gamma" }
tachys = { path = "./tachys", version = "0.1.0-gamma" }
[profile.release]
codegen-units = 1

View File

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

View File

@@ -2,7 +2,8 @@
name = "benchmarks"
version = "0.1.0"
edition = "2021"
rust-version.workspace = true
# std::sync::LazyLock is stabilized in Rust version 1.80.0
rust-version = "1.80.0"
[dependencies]
l0410 = { package = "leptos", version = "0.4.10", features = [

View File

@@ -18,7 +18,7 @@ fn leptos_ssr_bench(b: &mut Bencher) {
}
}
let rendered = view! {
let rendered = view! {
<main>
<h1>"Welcome to our benchmark page."</h1>
<p>"Here's some introductory text."</p>
@@ -58,7 +58,7 @@ fn tachys_ssr_bench(b: &mut Bencher) {
}
}
let rendered = view! {
let rendered = view! {
<main>
<h1>"Welcome to our benchmark page."</h1>
<p>"Here's some introductory text."</p>
@@ -92,13 +92,13 @@ fn tera_ssr_bench(b: &mut Bencher) {
{% endfor %}
</main>"#;
lazy_static::lazy_static! {
static ref TERA: Tera = {
let mut tera = Tera::default();
tera.add_raw_templates(vec![("template.html", TEMPLATE)]).unwrap();
tera
};
}
static LazyCell<TERA>: Tera = LazyLock::new(|| {
let mut tera = Tera::default();
tera.add_raw_templates(vec![("template.html", TEMPLATE)]).unwrap();
tera
});
#[derive(Serialize, Deserialize)]
struct Counter {

View File

@@ -55,7 +55,7 @@ static TEMPLATE: &str = r#"<main>
{% else %}
<li><a href="/">All</a></li>
{% endif %}
{% if mode_active %}
<li><a href="/active" class="selected">Active</a></li>
{% else %}
@@ -91,13 +91,13 @@ fn tera_todomvc_ssr(b: &mut Bencher) {
use serde::{Deserialize, Serialize};
use tera::*;
lazy_static::lazy_static! {
static ref TERA: Tera = {
static LazyLock<TERA>: Tera = LazyLock( || {
let mut tera = Tera::default();
tera.add_raw_templates(vec![("template.html", TEMPLATE)]).unwrap();
tera
};
}
});
#[derive(Serialize, Deserialize)]
struct Todo {
@@ -131,13 +131,13 @@ fn tera_todomvc_ssr_1000(b: &mut Bencher) {
use serde::{Deserialize, Serialize};
use tera::*;
lazy_static::lazy_static! {
static ref TERA: Tera = {
let mut tera = Tera::default();
tera.add_raw_templates(vec![("template.html", TEMPLATE)]).unwrap();
tera
};
}
static TERA: LazyLock<Tera> = LazyLock::new(|| {
let mut tera = Tera::default();
tera.add_raw_templates(vec![("template.html", TEMPLATE)]).unwrap();
tera
});
#[derive(Serialize, Deserialize)]
struct Todo {

View File

@@ -133,3 +133,104 @@ tuples!(EitherOf13 + EitherOf13Future + EitherOf13FutureProj => A, B, C, D, E, F
tuples!(EitherOf14 + EitherOf14Future + EitherOf14FutureProj => A, B, C, D, E, F, G, H, I, J, K, L, M, N);
tuples!(EitherOf15 + EitherOf15Future + EitherOf15FutureProj => A, B, C, D, E, F, G, H, I, J, K, L, M, N, O);
tuples!(EitherOf16 + EitherOf16Future + EitherOf16FutureProj => A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P);
/// Matches over the first expression and returns an either ([`Either`], [`EitherOf3`], ... [`EitherOf6`])
/// composed of the values returned by the match arms.
///
/// The pattern syntax is exactly the same as found in a match arm.
///
/// # Examples
///
/// ```
/// # use either_of::*;
/// let either2 = either!(Some("hello"),
/// Some(s) => s.len(),
/// None => 0.0,
/// );
/// assert!(matches!(either2, Either::<usize, f64>::Left(5)));
///
/// let either3 = either!(Some("admin"),
/// Some("admin") => "hello admin",
/// Some(_) => 'x',
/// _ => 0,
/// );
/// assert!(matches!(either3, EitherOf3::<&str, char, i32>::A("hello admin")));
/// ```
#[macro_export]
macro_rules! either {
($match:expr, $left_pattern:pat => $left_expression:expr, $right_pattern:pat => $right_expression:expr,) => {
match $match {
$left_pattern => $crate::Either::Left($left_expression),
$right_pattern => $crate::Either::Right($right_expression),
}
};
($match:expr, $a_pattern:pat => $a_expression:expr, $b_pattern:pat => $b_expression:expr, $c_pattern:pat => $c_expression:expr,) => {
match $match {
$a_pattern => $crate::EitherOf3::A($a_expression),
$b_pattern => $crate::EitherOf3::B($b_expression),
$c_pattern => $crate::EitherOf3::C($c_expression),
}
};
($match:expr, $a_pattern:pat => $a_expression:expr, $b_pattern:pat => $b_expression:expr, $c_pattern:pat => $c_expression:expr, $d_pattern:pat => $d_expression:expr,) => {
match $match {
$a_pattern => $crate::EitherOf4::A($a_expression),
$b_pattern => $crate::EitherOf4::B($b_expression),
$c_pattern => $crate::EitherOf4::C($c_expression),
$d_pattern => $crate::EitherOf4::D($d_expression),
}
};
($match:expr, $a_pattern:pat => $a_expression:expr, $b_pattern:pat => $b_expression:expr, $c_pattern:pat => $c_expression:expr, $d_pattern:pat => $d_expression:expr, $e_pattern:pat => $e_expression:expr,) => {
match $match {
$a_pattern => $crate::EitherOf5::A($a_expression),
$b_pattern => $crate::EitherOf5::B($b_expression),
$c_pattern => $crate::EitherOf5::C($c_expression),
$d_pattern => $crate::EitherOf5::D($d_expression),
$e_pattern => $crate::EitherOf5::E($e_expression),
}
};
($match:expr, $a_pattern:pat => $a_expression:expr, $b_pattern:pat => $b_expression:expr, $c_pattern:pat => $c_expression:expr, $d_pattern:pat => $d_expression:expr, $e_pattern:pat => $e_expression:expr, $f_pattern:pat => $f_expression:expr,) => {
match $match {
$a_pattern => $crate::EitherOf6::A($a_expression),
$b_pattern => $crate::EitherOf6::B($b_expression),
$c_pattern => $crate::EitherOf6::C($c_expression),
$d_pattern => $crate::EitherOf6::D($d_expression),
$e_pattern => $crate::EitherOf6::E($e_expression),
$f_pattern => $crate::EitherOf6::F($f_expression),
}
}; // if you need more eithers feel free to open a PR ;-)
}
// compile time test
#[test]
fn either_macro() {
let _: Either<&str, f64> = either!(12,
12 => "12",
_ => 0.0,
);
let _: EitherOf3<&str, f64, i32> = either!(12,
12 => "12",
13 => 0.0,
_ => 12,
);
let _: EitherOf4<&str, f64, char, i32> = either!(12,
12 => "12",
13 => 0.0,
14 => ' ',
_ => 12,
);
let _: EitherOf5<&str, f64, char, f32, i32> = either!(12,
12 => "12",
13 => 0.0,
14 => ' ',
15 => 0.0f32,
_ => 12,
);
let _: EitherOf6<&str, f64, char, f32, u8, i32> = either!(12,
12 => "12",
13 => 0.0,
14 => ' ',
15 => 0.0f32,
16 => 24u8,
_ => 12,
);
}

View File

@@ -0,0 +1,111 @@
[package]
name = "axum_js_ssr"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
axum = { version = "0.7.5", optional = true }
console_error_panic_hook = "0.1.7"
console_log = "1.0"
gloo-utils = "0.2.0"
html-escape = "0.2.13"
http-body-util = { version = "0.1.0", optional = true }
js-sys = { version = "0.3.69", optional = true }
leptos = { path = "../../leptos", features = ["tracing"] }
leptos_meta = { path = "../../meta" }
leptos_axum = { path = "../../integrations/axum", optional = true }
leptos_router = { path = "../../router" }
serde = { version = "1.0", features = ["derive"] }
thiserror = "1.0"
tokio = { version = "1.39", features = [ "rt-multi-thread", "macros", "time" ], optional = true }
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 }
[features]
hydrate = [
"leptos/hydrate",
"dep:js-sys",
"dep:web-sys",
]
ssr = [
"dep:axum",
"dep:http-body-util",
"dep:tower",
"dep:tower-http",
"dep:tokio",
"leptos/ssr",
"leptos_meta/ssr",
"dep:leptos_axum",
"leptos_router/ssr",
]
[profile.release]
panic = "abort"
[profile.wasm-release]
inherits = "release"
opt-level = 'z'
lto = true
codegen-units = 1
panic = "abort"
[package.metadata.cargo-all-features]
denylist = ["axum", "tower", "tower-http", "tokio", "sqlx", "leptos_axum"]
skip_feature_sets = [["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "axum_js_ssr"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
# [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/main.scss"
# Assets source dir. All files found here will be copied and synchronized to site-root.
# The assets-dir cannot have a sub directory with the same name/path as site-pkg-dir.
#
# Optional. Env: LEPTOS_ASSETS_DIR.
assets-dir = "assets"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "127.0.0.1:3000"
# The port to use for automatic reload monitoring
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
# [Windows] for non-WSL use "npx.cmd playwright test"
# This binary name can be checked in Powershell with Get-Command npx
end2end-cmd = "npx playwright test"
end2end-dir = "end2end"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target
#
# Optional. Can be over-ridden with the command line parameter --bin-features
bin-features = ["ssr"]
# If the --no-default-features flag should be used when compiling the bin target
#
# Optional. Defaults to false.
bin-default-features = false
# The features to use when compiling the lib target
#
# Optional. Can be over-ridden with the command line parameter --lib-features
lib-features = ["hydrate"]
# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false
lib-profile-release = "wasm-release"

View File

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

View File

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

View File

@@ -0,0 +1,10 @@
# Leptos Axum JS SSR Example
This example shows the various ways that JavaScript may be included into
a Leptos application. The intent is to demonstrate how this may be done
and how it may cause the application to fail in an unexpected manner if
done incorrectly.
## Quick Start
Run `cargo leptos watch` to run this example.

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2006, Ivan Sagalaev.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -0,0 +1,47 @@
# Highlight.js CDN Assets
**Note: this contains only a subset of files from the full package from NPM.**
[![install size](https://packagephobia.now.sh/badge?p=highlight.js)](https://packagephobia.now.sh/result?p=highlight.js)
**This package contains only the CDN build assets of highlight.js.**
This may be what you want if you'd like to install the pre-built distributable highlight.js client-side assets via NPM. If you're wanting to use highlight.js mainly on the server-side you likely want the [highlight.js][1] package instead.
To access these files via CDN:<br>
https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@latest/build/
**If you just want a single .js file with the common languages built-in:
<https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@latest/build/highlight.min.js>**
---
## Highlight.js
Highlight.js is a syntax highlighter written in JavaScript. It works in
the browser as well as on the server. It works with pretty much any
markup, doesnt depend on any framework, and has automatic language
detection.
If you'd like to read the full README:<br>
<https://github.com/highlightjs/highlight.js/blob/main/README.md>
## License
Highlight.js is released under the BSD License. See [LICENSE][7] file
for details.
## Links
The official site for the library is at <https://highlightjs.org/>.
The Github project may be found at: <https://github.com/highlightjs/highlight.js>
Further in-depth documentation for the API and other topics is at
<http://highlightjs.readthedocs.io/>.
A list of the Core Team and contributors can be found in the [CONTRIBUTORS.md][8] file.
[1]: https://www.npmjs.com/package/highlight.js
[7]: https://github.com/highlightjs/highlight.js/blob/main/LICENSE
[8]: https://github.com/highlightjs/highlight.js/blob/main/CONTRIBUTORS.md

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,93 @@
{
"name": "@highlightjs/cdn-assets",
"description": "Syntax highlighting with language autodetection. (pre-compiled CDN assets)",
"keywords": [
"highlight",
"syntax"
],
"homepage": "https://highlightjs.org/",
"version": "11.10.0",
"author": "Josh Goebel <hello@joshgoebel.com>",
"contributors": [
"Josh Goebel <hello@joshgoebel.com>",
"Egor Rogov <e.rogov@postgrespro.ru>",
"Vladimir Jimenez <me@allejo.io>",
"Ivan Sagalaev <maniac@softwaremaniacs.org>",
"Jeremy Hull <sourdrums@gmail.com>",
"Oleg Efimov <efimovov@gmail.com>",
"Gidi Meir Morris <gidi@gidi.io>",
"Jan T. Sott <git@idleberg.com>",
"Li Xuanji <xuanji@gmail.com>",
"Marcos Cáceres <marcos@marcosc.com>",
"Sang Dang <sang.dang@polku.io>"
],
"bugs": {
"url": "https://github.com/highlightjs/highlight.js/issues"
},
"license": "BSD-3-Clause",
"repository": {
"type": "git",
"url": "git://github.com/highlightjs/highlight.js.git"
},
"sideEffects": [
"./es/common.js",
"./lib/common.js",
"*.css",
"*.scss"
],
"scripts": {
"mocha": "mocha",
"lint": "eslint src/*.js src/lib/*.js demo/*.js tools/**/*.js --ignore-pattern vendor",
"lint-languages": "eslint --no-eslintrc -c .eslintrc.lang.js src/languages/**/*.js",
"build_and_test": "npm run build && npm run test",
"build_and_test_browser": "npm run build-browser && npm run test-browser",
"build": "node ./tools/build.js -t node",
"build-cdn": "node ./tools/build.js -t cdn",
"build-browser": "node ./tools/build.js -t browser :common",
"devtool": "npx http-server",
"test": "mocha test",
"test-markup": "mocha test/markup",
"test-detect": "mocha test/detect",
"test-browser": "mocha test/browser",
"test-parser": "mocha test/parser"
},
"engines": {
"node": ">=12.0.0"
},
"devDependencies": {
"@colors/colors": "^1.6.0",
"@rollup/plugin-commonjs": "^26.0.1",
"@rollup/plugin-json": "^6.0.1",
"@rollup/plugin-node-resolve": "^15.2.3",
"@types/mocha": "^10.0.2",
"@typescript-eslint/eslint-plugin": "^7.15.0",
"@typescript-eslint/parser": "^7.15.0",
"clean-css": "^5.3.2",
"cli-table": "^0.3.1",
"commander": "^12.1.0",
"css": "^3.0.0",
"css-color-names": "^1.0.1",
"deep-freeze-es6": "^3.0.2",
"del": "^7.1.0",
"dependency-resolver": "^2.0.1",
"eslint": "^8.57.0",
"eslint-config-standard": "^17.1.0",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^6.1.1",
"glob": "^8.1.0",
"glob-promise": "^6.0.5",
"handlebars": "^4.7.8",
"http-server": "^14.1.1",
"jsdom": "^24.1.0",
"lodash": "^4.17.20",
"mocha": "^10.2.0",
"refa": "^0.4.1",
"rollup": "^4.0.2",
"should": "^13.2.3",
"terser": "^5.21.0",
"tiny-worker": "^2.3.0",
"typescript": "^5.2.2",
"wcag-contrast": "^3.0.0"
}
}

View File

@@ -0,0 +1,10 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
Theme: GitHub Dark
Description: Dark theme as seen on github.com
Author: github.com
Maintainer: @Hirse
Updated: 2021-05-15
Outdated base version: https://github.com/primer/github-syntax-dark
Current colors taken from GitHub's CSS
*/.hljs{color:#c9d1d9;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#79c0ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-code,.hljs-comment,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c}

View File

@@ -0,0 +1,10 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
Theme: GitHub
Description: Light theme as seen on github.com
Author: github.com
Maintainer: @Hirse
Updated: 2021-05-15
Outdated base version: https://github.com/primer/github-syntax-light
Current colors taken from GitHub's CSS
*/.hljs{color:#24292e;background:#fff}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#d73a49}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#6f42c1}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#005cc5}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#032f62}.hljs-built_in,.hljs-symbol{color:#e36209}.hljs-code,.hljs-comment,.hljs-formula{color:#6a737d}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#22863a}.hljs-subst{color:#24292e}.hljs-section{color:#005cc5;font-weight:700}.hljs-bullet{color:#735c0f}.hljs-emphasis{color:#24292e;font-style:italic}.hljs-strong{color:#24292e;font-weight:700}.hljs-addition{color:#22863a;background-color:#f0fff4}.hljs-deletion{color:#b31d28;background-color:#ffeef0}

View File

@@ -0,0 +1,6 @@
{
"name": "axum_js_ssr",
"dependencies": {
"@highlightjs/cdn-assets": "^11.10.0"
}
}

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "stable" # test change

View File

@@ -0,0 +1,8 @@
use leptos::{prelude::ServerFnError, server};
#[server]
pub async fn fetch_code() -> Result<String, ServerFnError> {
// emulate loading of code from a database/version control/etc
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
Ok(crate::consts::CH05_02A.to_string())
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,39 @@
// Example programs from the Rust Programming Language Book
pub const CH03_05A: &str = r#"fn main() {
let number = 3;
if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
}
"#;
// For some reason, swapping the code examples "fixes" example 6. It
// might have something to do with the lower complexity of highlighting
// a shorter example. Anyway, including extra newlines for the shorter
// example to match with the longer in order to avoid reflowing the
// table during the async resource loading for CSR.
pub const CH05_02A: &str = r#"fn main() {
let width1 = 30;
let height1 = 50;
println!(
"The area of the rectangle is {} square pixels.",
area(width1, height1)
);
}
fn area(width: u32, height: u32) -> u32 {
width * height
}
"#;
pub const LEPTOS_HYDRATED: &str = "_leptos_hydrated";

View File

@@ -0,0 +1,59 @@
#[cfg(not(feature = "ssr"))]
mod csr {
use gloo_utils::format::JsValueSerdeExt;
use js_sys::{
Object,
Reflect::{get, set},
};
use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
#[wasm_bindgen(
module = "/node_modules/@highlightjs/cdn-assets/es/highlight.min.js"
)]
extern "C" {
type HighlightOptions;
#[wasm_bindgen(catch, js_namespace = default, js_name = highlight)]
fn highlight_lang(
code: String,
options: Object,
) -> Result<Object, JsValue>;
#[wasm_bindgen(js_namespace = default, js_name = highlightAll)]
pub fn highlight_all();
}
// Keeping the `ignoreIllegals` argument out of the default case, and since there is no optional arguments
// in Rust, this will have to be provided in a separate function (e.g. `highlight_ignore_illegals`), much
// like how `web_sys` does it for the browser APIs. For simplicity, only the highlighted HTML code is
// returned on success, and None on error.
pub fn highlight(code: String, lang: String) -> Option<String> {
let options = js_sys::Object::new();
set(&options, &"language".into(), &lang.into())
.expect("failed to assign lang to options");
highlight_lang(code, options)
.map(|result| {
let value = get(&result, &"value".into())
.expect("HighlightResult failed to contain the value key");
value.into_serde().expect("Value should have been a string")
})
.ok()
}
}
#[cfg(feature = "ssr")]
mod ssr {
// noop under ssr
pub fn highlight_all() {}
// TODO see if there is a Rust-based solution that will enable isomorphic rendering for this feature.
// the current (disabled) implementation simply calls html_escape.
// pub fn highlight(code: String, _lang: String) -> Option<String> {
// Some(html_escape::encode_text(&code).into_owned())
// }
}
#[cfg(not(feature = "ssr"))]
pub use csr::*;
#[cfg(feature = "ssr")]
pub use ssr::*;

View File

@@ -0,0 +1,51 @@
pub mod api;
pub mod app;
pub mod consts;
pub mod hljs;
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
use app::*;
use consts::LEPTOS_HYDRATED;
use std::panic;
panic::set_hook(Box::new(|info| {
// this custom hook will call out to show the usual error log at
// the console while also attempt to update the UI to indicate
// a restart of the application is required to continue.
console_error_panic_hook::hook(info);
let window = leptos::prelude::window();
if !matches!(
js_sys::Reflect::get(&window, &wasm_bindgen::JsValue::from_str(LEPTOS_HYDRATED)),
Ok(t) if t == true
) {
let document = leptos::prelude::document();
let _ = document.query_selector("#reset").map(|el| {
el.map(|el| {
el.set_class_name("panicked");
})
});
let _ = document.query_selector("#notice").map(|el| {
el.map(|el| {
el.set_class_name("panicked");
})
});
}
}));
leptos::mount::hydrate_body(App);
let window = leptos::prelude::window();
js_sys::Reflect::set(
&window,
&wasm_bindgen::JsValue::from_str(LEPTOS_HYDRATED),
&wasm_bindgen::JsValue::TRUE,
)
.expect("error setting hydrated status");
let event = web_sys::Event::new(LEPTOS_HYDRATED)
.expect("error creating hydrated event");
let document = leptos::prelude::document();
document
.dispatch_event(&event)
.expect("error dispatching hydrated event");
leptos::logging::log!("dispatched hydrated event");
}

View File

@@ -0,0 +1,152 @@
#[cfg(feature = "ssr")]
mod latency {
use std::sync::{Mutex, OnceLock};
pub static LATENCY: OnceLock<
Mutex<std::iter::Cycle<std::slice::Iter<'_, u64>>>,
> = OnceLock::new();
pub static ES_LATENCY: OnceLock<
Mutex<std::iter::Cycle<std::slice::Iter<'_, u64>>>,
> = OnceLock::new();
}
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use axum::{
body::Body,
extract::Request,
http::{
header::{self, HeaderValue},
StatusCode,
},
middleware::{self, Next},
response::{IntoResponse, Response},
routing::get,
Router,
};
use axum_js_ssr::app::*;
use http_body_util::BodyExt;
use leptos::prelude::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
latency::LATENCY.get_or_init(|| [0, 4, 40, 400].iter().cycle().into());
latency::ES_LATENCY.get_or_init(|| [0].iter().cycle().into());
// Having the ES_LATENCY (a cycle of latency for the loading of the es
// module) in an identical cycle as LATENCY (for the standard version)
// adversely influences the intended demo, as this ultimately delays
// hydration when set too high which can cause panic under every case.
// If you want to test the effects of the delay just modify the list of
// values for the desired cycle of delays.
let conf = get_configuration(None).unwrap();
let addr = conf.leptos_options.site_addr;
let leptos_options = conf.leptos_options;
// Generate the list of routes in your Leptos App
let routes = generate_route_list(App);
async fn highlight_js() -> impl IntoResponse {
(
[(header::CONTENT_TYPE, "text/javascript")],
include_str!(
"../node_modules/@highlightjs/cdn-assets/highlight.min.js"
),
)
}
async fn latency_for_highlight_js(
req: Request,
next: Next,
) -> Result<impl IntoResponse, (StatusCode, String)> {
let uri_parts = &mut req.uri().path().rsplit('/');
let is_highlightjs = uri_parts.next() == Some("highlight.min.js");
let es = uri_parts.next() == Some("es");
let module_type = if es { "es module " } else { "standard " };
let res = next.run(req).await;
if is_highlightjs {
// additional processing if the filename is the test subject
let (mut parts, body) = res.into_parts();
let bytes = body
.collect()
.await
.map_err(|err| {
(
StatusCode::BAD_REQUEST,
format!("error reading body: {err}"),
)
})?
.to_bytes();
let latency = if es {
&latency::ES_LATENCY
} else {
&latency::LATENCY
};
let delay = match latency
.get()
.expect("latency cycle wasn't set up")
.try_lock()
{
Ok(ref mut mutex) => {
*mutex.next().expect("cycle always has next")
}
Err(_) => 0,
};
// inject the logging of the delay used into the target script
log!(
"loading {module_type}highlight.min.js with latency of \
{delay} ms"
);
let js_log = format!(
"\nconsole.log('loaded {module_type}highlight.js with a \
minimum latency of {delay} ms');"
);
tokio::time::sleep(std::time::Duration::from_millis(delay)).await;
let bytes = [bytes, js_log.into()].concat();
let length = bytes.len();
let body = Body::from(bytes);
// Provide the bare minimum set of headers to avoid browser cache.
parts.headers = header::HeaderMap::from_iter(
[
(
header::CONTENT_TYPE,
HeaderValue::from_static("text/javascript"),
),
(header::CONTENT_LENGTH, HeaderValue::from(length)),
]
.into_iter(),
);
Ok(Response::from_parts(parts, body))
} else {
Ok(res)
}
}
let app = Router::new()
.route("/highlight.min.js", get(highlight_js))
.leptos_routes(&leptos_options, routes, {
let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone())
})
.fallback(leptos_axum::file_and_error_handler(shell))
.layer(middleware::from_fn(latency_for_highlight_js))
.with_state(leptos_options);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
log!("listening on http://{}", &addr);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}
#[cfg(not(feature = "ssr"))]
pub fn main() {
// no client-side main function
// unless we want this to work with e.g., Trunk for pure client-side testing
// see lib.rs for hydration function instead
}

View File

@@ -0,0 +1,171 @@
html, body {
margin: 0;
padding: 0;
font-family: sans-serif;
height: 100vh;
overflow: hidden;
}
body {
display: flex;
flex-flow: row nowrap;
}
nav {
min-width: 17em;
height: 100vh;
counter-reset: example-counter 0;
list-style-type: none;
list-style-position: outside;
overflow: auto;
}
nav a {
display: block;
padding: 0.5em 2em;
text-decoration: none;
}
nav a small {
display: block;
}
nav a.example::before {
counter-reset: subexample-counter 0;
counter-increment: example-counter 1;
content: counter(example-counter) ". ";
}
nav a.subexample::before {
counter-increment: subexample-counter 1;
content: counter(example-counter) "." counter(subexample-counter) " ";
}
div#notice {
display: none;
}
main div#notice.panicked {
position: sticky;
top: 0;
padding: 0.5em 2em;
display: block;
}
main {
width: 100%;
overflow: auto;
}
main article {
max-width: 60em;
margin: 0 1em;
padding: 0 1em;
}
main p, main li {
line-height: 1.3em;
}
main li pre code, main div pre code {
display: block;
line-height: normal;
}
main ol, main ul {
padding-left: 2em;
}
h2>code, p>code, li>code {
border-radius: 3px;
padding: 2px;
}
li pre code, div pre code {
margin: 0 !important;
padding: 0 !important;
}
#code-demo {
overflow-x: auto;
}
#code-demo table {
width: 50em;
margin: auto;
}
#code-demo table td {
vertical-align: top;
}
#code-demo table code {
display: block;
padding: 1em;
}
@media (prefers-color-scheme: light) {
nav {
background: #f7f7f7;
}
nav a {
color: #000;
}
nav a[aria-current="page"] {
background-color: #e0e0e0;
}
nav a:hover, h2>code, p>code, li>code {
background-color: #e7e7e7;
}
nav a.panicked, main div#notice.panicked {
background: #fdd;
}
main div#notice.panicked a {
color: #000;
}
nav a.section {
border-bottom: 1px solid #777;
}
}
@media (prefers-color-scheme: dark) {
nav {
background: #080808;
}
nav a {
color: #fff;
}
nav a[aria-current="page"] {
background-color: #3f3f3f;
}
nav a:hover, h2>code, p>code, li>code {
background-color: #383838;
}
nav a.panicked, main div#notice.panicked {
background: #733;
}
main div#notice.panicked a {
color: #fff;
}
nav a.section {
border-bottom: 1px solid #888;
}
}
// Just include the raw style as-is because I can't find a quick and easy way to import them just for the
// appropriate media type...
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}
@media (prefers-color-scheme: light){.hljs{color:#24292e;background:#fff}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#d73a49}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#6f42c1}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#005cc5}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#032f62}.hljs-built_in,.hljs-symbol{color:#e36209}.hljs-code,.hljs-comment,.hljs-formula{color:#6a737d}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#22863a}.hljs-subst{color:#24292e}.hljs-section{color:#005cc5;font-weight:700}.hljs-bullet{color:#735c0f}.hljs-emphasis{color:#24292e;font-style:italic}.hljs-strong{color:#24292e;font-weight:700}.hljs-addition{color:#22863a;background-color:#f0fff4}.hljs-deletion{color:#b31d28;background-color:#ffeef0}}
@media (prefers-color-scheme: dark){.hljs{color:#c9d1d9;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#79c0ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-code,.hljs-comment,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c}}

View File

@@ -1,7 +1,7 @@
use counter::*;
use leptos::mount::mount_to;
use leptos::prelude::*;
use leptos::spawn::tick;
use leptos::task::tick;
use wasm_bindgen::JsCast;
use wasm_bindgen_test::*;

View File

@@ -2,6 +2,8 @@
name = "counter_isomorphic"
version = "0.1.0"
edition = "2021"
# std::sync::LazyLock is stabilized in Rust version 1.80.0
rust-version = "1.80.0"
[lib]
crate-type = ["cdylib", "rlib"]
@@ -17,7 +19,6 @@ broadcaster = "1.0"
console_log = "1.0"
console_error_panic_hook = "0.1.7"
futures = "0.3.30"
lazy_static = "1.5"
leptos = { path = "../../leptos" }
leptos_actix = { path = "../../integrations/actix", optional = true }
leptos_router = { path = "../../router" }
@@ -46,13 +47,13 @@ denylist = ["actix-files", "actix-web", "leptos_actix"]
skip_feature_sets = [["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "counter_isomorphic"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
# When NOT using cargo-leptos this must be updated to "." or the counters will not work. The above warning still applies if you do switch to cargo-leptos later.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
# style-file = "src/styles/tailwind.css"

View File

@@ -1,4 +1,4 @@
use leptos::{prelude::*, reactive_graph::actions::Action};
use leptos::prelude::*;
use leptos_router::{
components::{FlatRoutes, Route, Router, A},
StaticSegment,
@@ -10,12 +10,12 @@ use tracing::instrument;
pub mod ssr_imports {
pub use broadcaster::BroadcastChannel;
pub use std::sync::atomic::{AtomicI32, Ordering};
use std::sync::LazyLock;
pub static COUNT: AtomicI32 = AtomicI32::new(0);
lazy_static::lazy_static! {
pub static ref COUNT_CHANNEL: BroadcastChannel<i32> = BroadcastChannel::new();
}
pub static COUNT_CHANNEL: LazyLock<BroadcastChannel<i32>> =
LazyLock::new(BroadcastChannel::<i32>::new);
}
#[server]

View File

@@ -1,5 +1,5 @@
use counter_without_macros::counter;
use leptos::{prelude::*, spawn::tick};
use leptos::{prelude::*, task::tick};
use pretty_assertions::assert_eq;
use wasm_bindgen::JsCast;
use wasm_bindgen_test::*;

View File

@@ -1,4 +1,4 @@
use leptos::prelude::{signal::*, *};
use leptos::prelude::*;
const MANY_COUNTERS: usize = 1000;

View File

@@ -4,7 +4,7 @@ use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
use counters::Counters;
use leptos::prelude::*;
use leptos::spawn::tick;
use leptos::task::tick;
use web_sys::HtmlElement;
#[wasm_bindgen_test]

View File

@@ -1,5 +1,5 @@
use directives::App;
use leptos::{prelude::*, spawn::tick};
use leptos::{prelude::*, task::tick};
use wasm_bindgen::JsCast;
use wasm_bindgen_test::*;
use web_sys::HtmlElement;

View File

@@ -1,18 +0,0 @@
[package]
name = "gtk"
version = "0.1.0"
edition = "2021"
[dependencies]
leptos = { path = "../../leptos" }
throw_error = { path = "../../any_error/" }
# these are used to build the integration
gtk = { version = "0.9.0", package = "gtk4" }
next_tuple = { path = "../../next_tuple/" }
paste = "1.0"
# we want to support using glib for the reactive runtime event loop
any_spawner = { path = "../../any_spawner/", features = ["glib"] }
# yes, we want effects to run: this is a "frontend," not a backend
reactive_graph = { path = "../../reactive_graph", features = ["effects"] }

View File

@@ -1,8 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta name="color-scheme" content="dark">
<link rel="css" href="style.css" data-trunk>
</head>
<body></body>
</html>

View File

@@ -1,627 +0,0 @@
use self::properties::Connect;
use gtk::{
ffi::GtkWidget,
glib::{
object::{IsA, IsClass, ObjectExt},
Object, Value,
},
prelude::{Cast, WidgetExt},
Label, Orientation, Widget,
};
use leptos::{
reactive_graph::effect::RenderEffect,
tachys::{
renderer::{CastFrom, Renderer},
view::{Mountable, Render},
},
};
use next_tuple::NextTuple;
use std::{borrow::Cow, marker::PhantomData};
#[derive(Debug)]
pub struct LeptosGtk;
#[derive(Debug, Clone)]
pub struct Element(pub Widget);
impl Element {
pub fn remove(&self) {
self.0.unparent();
}
}
#[derive(Debug, Clone)]
pub struct Text(pub Element);
impl<T> From<T> for Element
where
T: Into<Widget>,
{
fn from(value: T) -> Self {
Element(value.into())
}
}
impl Mountable<LeptosGtk> for Element {
fn unmount(&mut self) {
self.remove()
}
fn mount(
&mut self,
parent: &<LeptosGtk as Renderer>::Element,
marker: Option<&<LeptosGtk as Renderer>::Node>,
) {
self.0
.insert_before(&parent.0, marker.as_ref().map(|m| &m.0));
}
fn insert_before_this(&self, child: &mut dyn Mountable<LeptosGtk>) -> bool {
if let Some(parent) = self.0.parent() {
child.mount(&Element(parent), Some(self));
return true;
}
false
}
}
impl Mountable<LeptosGtk> for Text {
fn unmount(&mut self) {
self.0.remove()
}
fn mount(
&mut self,
parent: &<LeptosGtk as Renderer>::Element,
marker: Option<&<LeptosGtk as Renderer>::Node>,
) {
self.0
.0
.insert_before(&parent.0, marker.as_ref().map(|m| &m.0));
}
fn insert_before_this(&self, child: &mut dyn Mountable<LeptosGtk>) -> bool {
self.0.insert_before_this(child)
}
}
impl CastFrom<Element> for Element {
fn cast_from(source: Element) -> Option<Self> {
Some(source)
}
}
impl CastFrom<Element> for Text {
fn cast_from(source: Element) -> Option<Self> {
source
.0
.downcast::<Label>()
.ok()
.map(|n| Text(Element::from(n)))
}
}
impl AsRef<Element> for Element {
fn as_ref(&self) -> &Element {
self
}
}
impl AsRef<Element> for Text {
fn as_ref(&self) -> &Element {
&self.0
}
}
impl Renderer for LeptosGtk {
type Node = Element;
type Element = Element;
type Text = Text;
type Placeholder = Element;
fn intern(text: &str) -> &str {
text
}
fn create_text_node(text: &str) -> Self::Text {
Text(Element::from(Label::new(Some(text))))
}
fn create_placeholder() -> Self::Placeholder {
let label = Label::new(None);
label.set_visible(false);
Element::from(label)
}
fn set_text(node: &Self::Text, text: &str) {
let node_as_text = node.0 .0.downcast_ref::<Label>().unwrap();
node_as_text.set_label(text);
}
fn set_attribute(node: &Self::Element, name: &str, value: &str) {
node.0.set_property(name, value);
}
fn remove_attribute(node: &Self::Element, name: &str) {
node.0.set_property(name, None::<&str>);
}
fn insert_node(
parent: &Self::Element,
new_child: &Self::Node,
marker: Option<&Self::Node>,
) {
new_child
.0
.insert_before(&parent.0, marker.as_ref().map(|n| &n.0));
}
fn remove_node(
parent: &Self::Element,
child: &Self::Node,
) -> Option<Self::Node> {
todo!()
}
fn remove(node: &Self::Node) {
todo!()
}
fn get_parent(node: &Self::Node) -> Option<Self::Node> {
node.0.parent().map(Element::from)
}
fn first_child(node: &Self::Node) -> Option<Self::Node> {
todo!()
}
fn next_sibling(node: &Self::Node) -> Option<Self::Node> {
todo!()
}
fn log_node(node: &Self::Node) {
todo!()
}
fn clear_children(parent: &Self::Element) {
todo!()
}
}
pub fn root<Chil>(children: Chil) -> (Widget, impl Mountable<LeptosGtk>)
where
Chil: Render<LeptosGtk>,
{
let state = r#box()
.orientation(Orientation::Vertical)
.spacing(12)
.child(children)
.build();
(state.as_widget().clone(), state)
}
pub trait WidgetClass {
type Widget: Into<Widget> + IsA<Object> + IsClass;
}
pub struct LGtkWidget<Widg, Props, Chil> {
widget: PhantomData<Widg>,
properties: Props,
children: Chil,
}
impl<Widg, Props, Chil> LGtkWidget<Widg, Props, Chil>
where
Widg: WidgetClass,
Chil: NextTuple,
{
pub fn child<T>(
self,
child: T,
) -> LGtkWidget<Widg, Props, Chil::Output<T>> {
let LGtkWidget {
widget,
properties,
children,
} = self;
LGtkWidget {
widget,
properties,
children: children.next_tuple(child),
}
}
}
impl<Widg, Props, Chil> LGtkWidget<Widg, Props, Chil>
where
Widg: WidgetClass,
Props: NextTuple,
Chil: Render<LeptosGtk>,
{
pub fn connect<F>(
self,
signal_name: &'static str,
callback: F,
) -> LGtkWidget<Widg, Props::Output<Connect<F>>, Chil>
where
F: Fn(&[Value]) -> Option<Value> + Send + Sync + 'static,
{
let LGtkWidget {
widget,
properties,
children,
} = self;
LGtkWidget {
widget,
properties: properties.next_tuple(Connect {
signal_name,
callback,
}),
children,
}
}
}
pub struct LGtkWidgetState<Widg, Props, Chil>
where
Chil: Render<LeptosGtk>,
Props: Property,
Widg: WidgetClass,
{
ty: PhantomData<Widg>,
widget: Element,
properties: Props::State,
children: Chil::State,
}
impl<Widg, Props, Chil> LGtkWidgetState<Widg, Props, Chil>
where
Chil: Render<LeptosGtk>,
Props: Property,
Widg: WidgetClass,
{
pub fn as_widget(&self) -> &Widget {
&self.widget.0
}
}
impl<Widg, Props, Chil> Render<LeptosGtk> for LGtkWidget<Widg, Props, Chil>
where
Widg: WidgetClass,
Props: Property,
Chil: Render<LeptosGtk>,
{
type State = LGtkWidgetState<Widg, Props, Chil>;
fn build(self) -> Self::State {
let widget = Object::new::<Widg::Widget>();
let widget = Element::from(widget);
let properties = self.properties.build(&widget);
let mut children = self.children.build();
children.mount(&widget, None);
LGtkWidgetState {
ty: PhantomData,
widget,
properties,
children,
}
}
fn rebuild(self, state: &mut Self::State) {
self.properties
.rebuild(&state.widget, &mut state.properties);
self.children.rebuild(&mut state.children);
}
}
impl<Widg, Props, Chil> Mountable<LeptosGtk>
for LGtkWidgetState<Widg, Props, Chil>
where
Widg: WidgetClass,
Props: Property,
Chil: Render<LeptosGtk>,
{
fn unmount(&mut self) {
self.children.unmount();
self.widget.remove();
}
fn mount(
&mut self,
parent: &<LeptosGtk as Renderer>::Element,
marker: Option<&<LeptosGtk as Renderer>::Node>,
) {
self.children.mount(&self.widget, None);
LeptosGtk::insert_node(parent, &self.widget, marker);
}
fn insert_before_this(&self, child: &mut dyn Mountable<LeptosGtk>) -> bool {
self.widget.insert_before_this(child)
}
}
pub trait Property {
type State;
fn build(self, element: &Element) -> Self::State;
fn rebuild(self, element: &Element, state: &mut Self::State);
}
impl<T, F> Property for F
where
T: Property,
T::State: 'static,
F: Fn() -> T + 'static,
{
type State = RenderEffect<T::State>;
fn build(self, widget: &Element) -> Self::State {
let widget = widget.clone();
RenderEffect::new(move |prev| {
let value = self();
if let Some(mut prev) = prev {
value.rebuild(&widget, &mut prev);
prev
} else {
value.build(&widget)
}
})
}
fn rebuild(self, widget: &Element, state: &mut Self::State) {}
}
pub fn button() -> LGtkWidget<gtk::Button, (), ()> {
LGtkWidget {
widget: PhantomData,
properties: (),
children: (),
}
}
pub fn r#box() -> LGtkWidget<gtk::Box, (), ()> {
LGtkWidget {
widget: PhantomData,
properties: (),
children: (),
}
}
mod widgets {
use super::WidgetClass;
impl WidgetClass for gtk::Button {
type Widget = Self;
}
impl WidgetClass for gtk::Box {
type Widget = Self;
}
}
pub mod properties {
use super::{
Element, LGtkWidget, LGtkWidgetState, LeptosGtk, Property, WidgetClass,
};
use gtk::glib::{object::ObjectExt, Value};
use leptos::tachys::{renderer::Renderer, view::Render};
use next_tuple::NextTuple;
pub struct Connect<F>
where
F: Fn(&[Value]) -> Option<Value> + Send + Sync + 'static,
{
pub signal_name: &'static str,
pub callback: F,
}
impl<F> Property for Connect<F>
where
F: Fn(&[Value]) -> Option<Value> + Send + Sync + 'static,
{
type State = ();
fn build(self, element: &Element) -> Self::State {
element.0.connect(self.signal_name, false, self.callback);
}
fn rebuild(self, element: &Element, state: &mut Self::State) {}
}
/* examples for macro */
pub struct Orientation {
value: gtk::Orientation,
}
pub struct OrientationState {
value: gtk::Orientation,
}
impl Property for Orientation {
type State = OrientationState;
fn build(self, element: &Element) -> Self::State {
element.0.set_property("orientation", self.value);
OrientationState { value: self.value }
}
fn rebuild(self, element: &Element, state: &mut Self::State) {
if self.value != state.value {
element.0.set_property("orientation", self.value);
state.value = self.value;
}
}
}
impl<Widg, Props, Chil> LGtkWidget<Widg, Props, Chil>
where
Widg: WidgetClass,
Props: NextTuple,
Chil: Render<LeptosGtk>,
{
pub fn orientation(
self,
value: impl Into<gtk::Orientation>,
) -> LGtkWidget<Widg, Props::Output<Orientation>, Chil> {
let LGtkWidget {
widget,
properties,
children,
} = self;
LGtkWidget {
widget,
properties: properties.next_tuple(Orientation {
value: value.into(),
}),
children,
}
}
}
pub struct Spacing {
value: i32,
}
pub struct SpacingState {
value: i32,
}
impl Property for Spacing {
type State = SpacingState;
fn build(self, element: &Element) -> Self::State {
element.0.set_property("spacing", self.value);
SpacingState { value: self.value }
}
fn rebuild(self, element: &Element, state: &mut Self::State) {
if self.value != state.value {
element.0.set_property("spacing", self.value);
state.value = self.value;
}
}
}
impl<Widg, Props, Chil> LGtkWidget<Widg, Props, Chil>
where
Widg: WidgetClass,
Props: NextTuple,
Chil: Render<LeptosGtk>,
{
pub fn spacing(
self,
value: impl Into<i32>,
) -> LGtkWidget<Widg, Props::Output<Spacing>, Chil> {
let LGtkWidget {
widget,
properties,
children,
} = self;
LGtkWidget {
widget,
properties: properties.next_tuple(Spacing {
value: value.into(),
}),
children,
}
}
}
/* end examples for properties macro */
pub struct Label {
value: String,
}
impl Label {
pub fn new(value: impl Into<String>) -> Self {
Self {
value: value.into(),
}
}
}
pub struct LabelState {
value: String,
}
impl Property for Label {
type State = LabelState;
fn build(self, element: &Element) -> Self::State {
LeptosGtk::set_attribute(element, "label", &self.value);
LabelState { value: self.value }
}
fn rebuild(self, element: &Element, state: &mut Self::State) {
todo!()
}
}
impl Property for () {
type State = ();
fn build(self, _element: &Element) -> Self::State {}
fn rebuild(self, _element: &Element, _state: &mut Self::State) {}
}
macro_rules! tuples {
($($ty:ident),* $(,)?) => {
impl<$($ty,)*> Property for ($($ty,)*)
where $($ty: Property,)*
{
type State = ($($ty::State,)*);
fn build(self, element: &Element) -> Self::State {
#[allow(non_snake_case)]
let ($($ty,)*) = self;
($($ty.build(element),)*)
}
fn rebuild(self, element: &Element, state: &mut Self::State) {
paste::paste! {
#[allow(non_snake_case)]
let ($($ty,)*) = self;
#[allow(non_snake_case)]
let ($([<state_ $ty:lower>],)*) = state;
$($ty.rebuild(element, [<state_ $ty:lower>]));*
}
}
}
}
}
tuples!(A);
tuples!(A, B);
tuples!(A, B, C);
tuples!(A, B, C, D);
tuples!(A, B, C, D, E);
tuples!(A, B, C, D, E, F);
tuples!(A, B, C, D, E, F, G);
tuples!(A, B, C, D, E, F, G, H);
tuples!(A, B, C, D, E, F, G, H, I);
tuples!(A, B, C, D, E, F, G, H, I, J);
tuples!(A, B, C, D, E, F, G, H, I, J, K);
tuples!(A, B, C, D, E, F, G, H, I, J, K, L);
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M);
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N);
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O);
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P);
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q);
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R);
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S);
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T);
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U);
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V);
tuples!(
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W
);
tuples!(
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X
);
tuples!(
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X,
Y
);
}

View File

@@ -1,107 +0,0 @@
use any_spawner::Executor;
use gtk::{prelude::*, Application, ApplicationWindow, Orientation};
use leptos::prelude::*;
use leptos_gtk::LeptosGtk;
use std::{mem, thread, time::Duration};
mod leptos_gtk;
const APP_ID: &str = "dev.leptos.Counter";
// Basic GTK app setup from https://gtk-rs.org/gtk4-rs/stable/latest/book/hello_world.html
fn main() {
// use the glib event loop to power the reactive system
_ = Executor::init_glib();
let app = Application::builder().application_id(APP_ID).build();
app.connect_startup(|_| load_css());
app.connect_activate(|app| {
// Connect to "activate" signal of `app`
let owner = Owner::new();
let view = owner.with(ui);
let (root, state) = leptos_gtk::root(view);
let window = ApplicationWindow::builder()
.application(app)
.title("TachyGTK")
.child(&root)
.build();
// Present window
window.present();
mem::forget((owner, state));
});
app.run();
}
fn ui() -> impl Render<LeptosGtk> {
let value = RwSignal::new(0);
let rows = RwSignal::new(vec![1, 2, 3, 4, 5]);
Effect::new(move |_| {
println!("value = {}", value.get());
});
// just an example of multithreaded reactivity
thread::spawn(move || loop {
thread::sleep(Duration::from_millis(250));
value.update(|n| *n += 1);
});
vstack((
hstack((
button("-1", move || {
println!("clicked -1");
value.update(|n| *n -= 1);
}),
move || value.get().to_string(),
button("+1", move || value.update(|n| *n += 1)),
)),
button("Swap", move || {
rows.update(|items| {
items.swap(1, 3);
})
}),
hstack(rows),
))
}
fn button(
label: impl Render<LeptosGtk>,
callback: impl Fn() + Send + Sync + 'static,
) -> impl Render<LeptosGtk> {
leptos_gtk::button()
.child(label)
.connect("clicked", move |_| {
callback();
None
})
}
fn vstack(children: impl Render<LeptosGtk>) -> impl Render<LeptosGtk> {
leptos_gtk::r#box()
.orientation(Orientation::Vertical)
.spacing(12)
.child(children)
}
fn hstack(children: impl Render<LeptosGtk>) -> impl Render<LeptosGtk> {
leptos_gtk::r#box()
.orientation(Orientation::Horizontal)
.spacing(12)
.child(children)
}
fn load_css() {
use gtk::{gdk::Display, CssProvider};
let provider = CssProvider::new();
provider.load_from_path("style.css");
// Add the provider to the default screen
gtk::style_context_add_provider_for_display(
&Display::default().expect("Could not connect to a display."),
&provider,
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
);
}

View File

View File

@@ -31,7 +31,6 @@ tokio = { version = "1.39", features = ["full"], optional = true }
http = { version = "1.1", optional = true }
web-sys = { version = "0.3.70", features = ["AbortController", "AbortSignal"] }
wasm-bindgen = "0.2.93"
lazy_static = "1.5"
rust-embed = { version = "8.5", features = [
"axum",
"mime_guess",

View File

@@ -17,7 +17,7 @@ leptos = { path = "../../leptos", features = [
leptos_router = { path = "../../router" }
server_fn = { path = "../../server_fn", features = ["serde-lite"] }
leptos_axum = { path = "../../integrations/axum", features = [
"islands-router",
"dont-use-islands-router",
], optional = true }
log = "0.4.22"
serde = { version = "1.0", features = ["derive"] }

View File

@@ -1,19 +1,14 @@
# Leptos Todo App Sqlite with Axum
# Work in Progress
This example creates a basic todo app with an Axum backend that uses Leptos' server functions to call sqlx from the client and seamlessly run it on the server.
This example is something I wrote on a long layover in the Orlando airport in July. (It was really hot!)
## Getting Started
It is the culmination of a couple years of thinking and working toward being able to do this, which you can see
described pretty well in the pinned roadmap issue (#1830) and its discussion of different modes of client-side
routing when you use islands.
See the [Examples README](../README.md) for setup and run instructions.
This uses *only* server rendering, with no actual islands, but still maintains client-side state across page navigations.
It does this by building on the fact that we now have a statically-typed view tree to do pretty smart updates with
new HTML from the client, with extremely minimal diffing.
## E2E Testing
See the [E2E README](./e2e/README.md) for more information about the testing strategy.
## Rendering
See the [SSR Notes](../SSR_NOTES.md) for more information about Server Side Rendering.
## Quick Start
Run `cargo leptos watch` to run this example.
The demo itself works, but the feature that supports it is incomplete. A couple people have accidentally
used it and broken their applications in ways they don't understand, so I've renamed the feature to `dont-use-islands-router`.

View File

@@ -162,22 +162,24 @@ pub fn App() -> impl IntoView {
<table class="table table-hover table-striped test-data">
<tbody>
<For
each={move || data.get()}
key={|row| row.id}
each=move || data.get()
key=|row| row.id
children=move |row: RowData| {
let row_id = row.id;
let label = row.label;
let is_selected = is_selected.clone();
ViewTemplate::new(view! {
<tr class:danger={move || is_selected.selected(Some(row_id))}>
<td class="col-md-1">{row_id.to_string()}</td>
<td class="col-md-4"><a on:click=move |_| set_selected.set(Some(row_id))>{move || label.get()}</a></td>
<td class="col-md-1"><a on:click=move |_| remove(row_id)><span class="glyphicon glyphicon-remove" aria-hidden="true"></span></a></td>
<td class="col-md-6"/>
</tr>
})
template! {
< tr class : danger = { move || is_selected.selected(Some(row_id)) }
> < td class = "col-md-1" > { row_id.to_string() } </ td > < td
class = "col-md-4" >< a on : click = move | _ | set_selected
.set(Some(row_id)) > { move || label.get() } </ a ></ td > < td
class = "col-md-1" >< a on : click = move | _ | remove(row_id) ><
span class = "glyphicon glyphicon-remove" aria - hidden = "true" ></
span ></ a ></ td > < td class = "col-md-6" /> </ tr >
}
}
/>
</tbody>
</table>
<span class="preloadicon glyphicon glyphicon-remove" aria-hidden="true"></span>

View File

@@ -1,5 +1,5 @@
use js_framework_benchmark_leptos::*;
use leptos::{prelude::*, spawn::tick};
use leptos::{prelude::*, task::tick};
use wasm_bindgen::JsCast;
use wasm_bindgen_test::*;

View File

@@ -3,7 +3,7 @@ use wasm_bindgen::JsCast;
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
use leptos::spawn::tick;
use leptos::task::tick;
use leptos::{leptos_dom::helpers::document, mount::mount_to};
use web_sys::HtmlButtonElement;

View File

@@ -65,7 +65,7 @@ pub fn RouterExample() -> impl IntoView {
// You can define other routes in their own component.
// Routes implement the MatchNestedRoutes
#[component]
pub fn ContactRoutes() -> impl MatchNestedRoutes<Dom> + Clone {
pub fn ContactRoutes() -> impl MatchNestedRoutes + Clone {
view! {
<ParentRoute path=path!("") view=ContactList>
<Route path=path!("/") view=|| "Select a contact."/>

View File

@@ -1,6 +1,6 @@
use futures::StreamExt;
use http::Method;
use leptos::{html::Input, prelude::*, spawn::spawn_local};
use leptos::{html::Input, prelude::*, task::spawn_local};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use server_fn::{
client::{browser::BrowserClient, Client},

View File

@@ -2,6 +2,8 @@
name = "ssr_modes"
version = "0.1.0"
edition = "2021"
# std::sync::LazyLock is stabilized in Rust version 1.80.0
rust-version = "1.80.0"
[lib]
crate-type = ["cdylib", "rlib"]
@@ -11,7 +13,6 @@ actix-files = { version = "0.6.6", optional = true }
actix-web = { version = "4.8", optional = true, features = ["macros"] }
console_error_panic_hook = "0.1.7"
console_log = "1.0"
lazy_static = "1.5"
leptos = { path = "../../leptos" }
leptos_meta = { path = "../../meta" }
leptos_actix = { path = "../../integrations/actix", optional = true }
@@ -38,12 +39,12 @@ denylist = ["actix-files", "actix-web", "leptos_actix"]
skip_feature_sets = [["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "ssr_modes"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "style/main.scss"

View File

@@ -1,4 +1,5 @@
use lazy_static::lazy_static;
use std::sync::LazyLock;
use leptos::prelude::*;
use leptos_meta::*;
use leptos_router::{
@@ -146,8 +147,9 @@ fn Post() -> impl IntoView {
}
// Dummy API
lazy_static! {
static ref POSTS: Vec<Post> = vec![
static POSTS: LazyLock<[Post; 3]> = LazyLock::new(|| {
[
Post {
id: 0,
title: "My first post".to_string(),
@@ -163,8 +165,8 @@ lazy_static! {
title: "My third post".to_string(),
content: "This is my third post".to_string(),
},
];
}
]
});
#[derive(Error, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PostError {

View File

@@ -2,6 +2,8 @@
name = "ssr_modes_axum"
version = "0.1.0"
edition = "2021"
# std::sync::LazyLock is stabilized in Rust version 1.80.0
rust-version = "1.80.0"
[lib]
crate-type = ["cdylib", "rlib"]
@@ -9,7 +11,6 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
console_error_panic_hook = "0.1.7"
console_log = "1.0"
lazy_static = "1.5"
leptos = { path = "../../leptos", features = [
"hydration",
] } #"nightly", "hydration"] }

View File

@@ -1,4 +1,5 @@
use lazy_static::lazy_static;
use std::sync::LazyLock;
use leptos::prelude::*;
use leptos_meta::MetaTags;
use leptos_meta::*;
@@ -261,8 +262,9 @@ pub fn Admin() -> impl IntoView {
}
// Dummy API
lazy_static! {
static ref POSTS: Vec<Post> = vec![
static POSTS: LazyLock<[Post; 3]> = LazyLock::new(|| {
[
Post {
id: 0,
title: "My first post".to_string(),
@@ -278,8 +280,8 @@ lazy_static! {
title: "My third post".to_string(),
content: "This is my third post".to_string(),
},
];
}
]
});
#[derive(Error, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PostError {

13
examples/static_routing/.gitignore vendored Normal file
View File

@@ -0,0 +1,13 @@
# Generated by Cargo
# will have compiled files and executables
/target/
pkg
# These are backup files generated by rustfmt
**/*.rs.bk
# node e2e test tools and outputs
node_modules/
test-results/
end2end/playwright-report/
playwright/.cache/

View File

@@ -0,0 +1,115 @@
[package]
name = "static_routing"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
console_error_panic_hook = "0.1.7"
console_log = "1.0"
leptos = { path = "../../leptos", features = [
"hydration",
] } #"nightly", "hydration"] }
leptos_meta = { path = "../../meta" }
leptos_axum = { path = "../../integrations/axum", optional = true }
leptos_router = { path = "../../router" }
log = "0.4.22"
serde = { version = "1.0", features = ["derive"] }
thiserror = "1.0"
axum = { version = "0.7.5", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
tokio = { version = "1.39", features = [
"fs",
"rt-multi-thread",
"macros",
], optional = true }
tokio-stream = { version = "0.1", features = ["fs"], optional = true }
futures = "0.3"
wasm-bindgen = "0.2.93"
notify = { version = "6", optional = true }
http = { version = "1", optional = true }
[features]
hydrate = ["leptos/hydrate"]
ssr = [
"dep:axum",
"dep:tower",
"dep:tower-http",
"dep:tokio",
"dep:tokio-stream",
"leptos/ssr",
"leptos_meta/ssr",
"dep:leptos_axum",
"leptos_router/ssr",
"dep:notify",
"dep:http"
]
[profile.release]
panic = "abort"
[profile.wasm-release]
inherits = "release"
opt-level = 'z'
lto = true
codegen-units = 1
panic = "abort"
[package.metadata.cargo-all-features]
denylist = ["axum", "tower", "tower-http", "tokio", "sqlx", "leptos_axum"]
skip_feature_sets = [["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "ssr_modes"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
# [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/main.scss"
# Assets source dir. All files found here will be copied and synchronized to site-root.
# The assets-dir cannot have a sub directory with the same name/path as site-pkg-dir.
#
# Optional. Env: LEPTOS_ASSETS_DIR.
assets-dir = "assets"
# 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:3007"
# The port to use for automatic reload monitoring
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
# [Windows] for non-WSL use "npx.cmd playwright test"
# This binary name can be checked in Powershell with Get-Command npx
end2end-cmd = "npx playwright test"
end2end-dir = "end2end"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target
#
# Optional. Can be over-ridden with the command line parameter --bin-features
bin-features = ["ssr"]
# If the --no-default-features flag should be used when compiling the bin target
#
# Optional. Defaults to false.
bin-default-features = false
# The features to use when compiling the lib target
#
# Optional. Can be over-ridden with the command line parameter --lib-features
lib-features = ["hydrate"]
# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false
lib-profile-release = "wasm-release"

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
# Static Routing Example
This example shows the static routing features, which can be used to generate the HTML content for some routes before a request.
## Getting Started
See the [Examples README](../README.md) for setup and run instructions.
## Quick Start
Run `cargo leptos watch` to run this example.

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,3 @@
# My first blog post
Having a blog is *fun*.

View File

@@ -0,0 +1,3 @@
# My second blog post
Coming up with content is hard.

View File

@@ -0,0 +1,3 @@
# My third blog post
Could I just have AI write this for me instead?

View File

@@ -0,0 +1,3 @@
# My fourth post
Here is some content. It should regenerate the static page.

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "stable" # test change

View File

@@ -0,0 +1,323 @@
use std::path::Path;
use futures::{channel::mpsc, Stream};
use leptos::prelude::*;
use leptos_meta::MetaTags;
use leptos_meta::*;
use leptos_router::{
components::{FlatRoutes, Redirect, Route, Router},
hooks::use_params,
params::Params,
path,
static_routes::StaticRoute,
SsrMode,
};
use serde::{Deserialize, Serialize};
use thiserror::Error;
pub fn shell(options: LeptosOptions) -> impl IntoView {
view! {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<AutoReload options=options.clone()/>
<HydrationScripts options/>
<MetaTags/>
</head>
<body>
<App/>
</body>
</html>
}
}
#[component]
pub fn App() -> impl IntoView {
// Provides context that manages stylesheets, titles, meta tags, etc.
provide_meta_context();
let fallback = || view! { "Page not found." }.into_view();
view! {
<Stylesheet id="leptos" href="/pkg/ssr_modes.css"/>
<Title text="Welcome to Leptos"/>
<Meta name="color-scheme" content="dark light"/>
<Router>
<nav>
<a href="/">"Home"</a>
</nav>
<main>
<FlatRoutes fallback>
<Route
path=path!("/")
view=HomePage
ssr=SsrMode::Static(
StaticRoute::new().regenerate(|_| watch_path(Path::new("./posts"))),
)
/>
<Route
path=path!("/about")
view=move || view! { <Redirect path="/"/> }
ssr=SsrMode::Static(StaticRoute::new())
/>
<Route
path=path!("/post/:slug/")
view=Post
ssr=SsrMode::Static(
StaticRoute::new()
.prerender_params(|| async move {
[("slug".into(), list_slugs().await.unwrap_or_default())]
.into_iter()
.collect()
})
.regenerate(|params| {
let slug = params.get("slug").unwrap();
watch_path(Path::new(&format!("./posts/{slug}.md")))
}),
)
/>
</FlatRoutes>
</main>
</Router>
}
}
#[component]
fn HomePage() -> impl IntoView {
// load the posts
let posts = Resource::new(|| (), |_| list_posts());
let posts = move || {
posts
.get()
.map(|n| n.unwrap_or_default())
.unwrap_or_default()
};
view! {
<h1>"My Great Blog"</h1>
<Suspense fallback=move || view! { <p>"Loading posts..."</p> }>
<ul>
<For each=posts key=|post| post.slug.clone() let:post>
<li>
<a href=format!("/post/{}/", post.slug)>{post.title.clone()}</a>
</li>
</For>
</ul>
</Suspense>
}
}
#[derive(Params, Clone, Debug, PartialEq, Eq)]
pub struct PostParams {
slug: Option<String>,
}
#[component]
fn Post() -> impl IntoView {
let query = use_params::<PostParams>();
let slug = move || {
query
.get()
.map(|q| q.slug.unwrap_or_default())
.map_err(|_| PostError::InvalidId)
};
let post_resource = Resource::new_blocking(slug, |slug| async move {
match slug {
Err(e) => Err(e),
Ok(slug) => get_post(slug)
.await
.map(|data| data.ok_or(PostError::PostNotFound))
.map_err(|e| PostError::ServerError(e.to_string())),
}
});
let post_view = move || {
Suspend::new(async move {
match post_resource.await {
Ok(Ok(post)) => {
Ok(view! {
<h1>{post.title.clone()}</h1>
<p>{post.content.clone()}</p>
// since we're using async rendering for this page,
// this metadata should be included in the actual HTML <head>
// when it's first served
<Title text=post.title/>
<Meta name="description" content=post.content/>
})
}
Ok(Err(e)) | Err(e) => {
Err(PostError::ServerError(e.to_string()))
}
}
})
};
view! {
<em>"The world's best content."</em>
<Suspense fallback=move || view! { <p>"Loading post..."</p> }>
<ErrorBoundary fallback=|errors| {
#[cfg(feature = "ssr")]
expect_context::<leptos_axum::ResponseOptions>()
.set_status(http::StatusCode::NOT_FOUND);
view! {
<div class="error">
<h1>"Something went wrong."</h1>
<ul>
{move || {
errors
.get()
.into_iter()
.map(|(_, error)| view! { <li>{error.to_string()}</li> })
.collect::<Vec<_>>()
}}
</ul>
</div>
}
}>{post_view}</ErrorBoundary>
</Suspense>
}
}
#[derive(Error, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PostError {
#[error("Invalid post ID.")]
InvalidId,
#[error("Post not found.")]
PostNotFound,
#[error("Server error: {0}.")]
ServerError(String),
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Post {
slug: String,
title: String,
content: String,
}
#[server]
pub async fn list_slugs() -> Result<Vec<String>, ServerFnError> {
use tokio::fs;
use tokio_stream::wrappers::ReadDirStream;
use tokio_stream::StreamExt;
let files = ReadDirStream::new(fs::read_dir("./posts").await?);
Ok(files
.filter_map(|entry| {
let entry = entry.ok()?;
let path = entry.path();
if !path.is_file() {
return None;
}
let extension = path.extension()?;
if extension != "md" {
return None;
}
let slug = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or_default()
.replace(".md", "");
Some(slug)
})
.collect()
.await)
}
#[server]
pub async fn list_posts() -> Result<Vec<Post>, ServerFnError> {
println!("calling list_posts");
use futures::TryStreamExt;
use tokio::fs;
use tokio_stream::wrappers::ReadDirStream;
let files = ReadDirStream::new(fs::read_dir("./posts").await?);
files
.try_filter_map(|entry| async move {
let path = entry.path();
if !path.is_file() {
return Ok(None);
}
let Some(extension) = path.extension() else {
return Ok(None);
};
if extension != "md" {
return Ok(None);
}
let slug = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or_default()
.replace(".md", "");
let content = fs::read_to_string(path).await?;
// world's worst Markdown frontmatter parser
let title = content.lines().next().unwrap().replace("# ", "");
Ok(Some(Post {
slug,
title,
content,
}))
})
.try_collect()
.await
.map_err(ServerFnError::from)
}
#[server]
pub async fn get_post(slug: String) -> Result<Option<Post>, ServerFnError> {
println!("reading ./posts/{slug}.md");
let content =
tokio::fs::read_to_string(&format!("./posts/{slug}.md")).await?;
// world's worst Markdown frontmatter parser
let title = content.lines().next().unwrap().replace("# ", "");
Ok(Some(Post {
slug,
title,
content,
}))
}
#[allow(unused)] // path is not used in non-SSR
fn watch_path(path: &Path) -> impl Stream<Item = ()> {
#[allow(unused)]
let (mut tx, rx) = mpsc::channel(0);
#[cfg(feature = "ssr")]
{
use notify::RecursiveMode;
use notify::Watcher;
let mut watcher =
notify::recommended_watcher(move |res: Result<_, _>| {
if res.is_ok() {
// if this fails, it's because the buffer is full
// this means we've already notified before it's regenerated,
// so this page will be queued for regeneration already
_ = tx.try_send(());
}
})
.expect("could not create watcher");
// Add a path to be watched. All files and directories at that path and
// below will be monitored for changes.
watcher
.watch(path, RecursiveMode::NonRecursive)
.expect("could not watch path");
// we want this to run as long as the server is alive
std::mem::forget(watcher);
}
rx
}

View File

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

View File

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

View File

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

View File

@@ -13,6 +13,9 @@ leptos = { path = "../../leptos", features = ["csr"] }
reactive_stores = { path = "../../reactive_stores" }
reactive_stores_macro = { path = "../../reactive_stores_macro" }
console_error_panic_hook = "0.1.7"
chrono = { version = "0.4.38", features = ["serde"] }
serde = { version = "1.0.210", features = ["derive"] }
serde_json = "1.0.128"
[dev-dependencies]
wasm-bindgen = "0.2.93"

View File

@@ -3,6 +3,11 @@
<head>
<link data-trunk rel="rust" data-wasm-opt="z"/>
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
<style>
.hidden {
display: none;
}
</style>
</head>
<body></body>
</html>
</html>

View File

@@ -1,43 +1,88 @@
use leptos::prelude::*;
use reactive_stores::{Field, Store, StoreFieldIterator};
use reactive_stores_macro::Store;
use std::sync::atomic::{AtomicUsize, Ordering};
#[derive(Debug, Store)]
use chrono::{Local, NaiveDate};
use leptos::prelude::*;
use reactive_stores::{Field, Patch, Store};
use reactive_stores_macro::{Patch, Store};
use serde::{Deserialize, Serialize};
// ID starts higher than 0 because we have a few starting todos by default
static NEXT_ID: AtomicUsize = AtomicUsize::new(3);
#[derive(Debug, Store, Serialize, Deserialize)]
struct Todos {
user: String,
user: User,
#[store(key: usize = |todo| todo.id)]
todos: Vec<Todo>,
}
#[derive(Debug, Store)]
#[derive(Debug, Store, Patch, Serialize, Deserialize)]
struct User {
name: String,
email: String,
}
#[derive(Debug, Store, Serialize, Deserialize)]
struct Todo {
id: usize,
label: String,
completed: bool,
status: Status,
}
#[derive(Debug, Default, Clone, Store, Serialize, Deserialize)]
enum Status {
#[default]
Pending,
Scheduled,
ScheduledFor {
date: NaiveDate,
},
Done,
}
impl Status {
pub fn next_step(&mut self) {
*self = match self {
Status::Pending => Status::ScheduledFor {
date: Local::now().naive_local().into(),
},
Status::Scheduled | Status::ScheduledFor { .. } => Status::Done,
Status::Done => Status::Done,
};
}
}
impl Todo {
pub fn new(label: impl ToString) -> Self {
Self {
id: NEXT_ID.fetch_add(1, Ordering::Relaxed),
label: label.to_string(),
completed: false,
status: Status::Pending,
}
}
}
fn data() -> Todos {
Todos {
user: "Bob".to_string(),
user: User {
name: "Bob".to_string(),
email: "lawblog@bobloblaw.com".into(),
},
todos: vec![
Todo {
id: 0,
label: "Create reactive store".to_string(),
completed: true,
status: Status::Pending,
},
Todo {
id: 1,
label: "???".to_string(),
completed: false,
status: Status::Pending,
},
Todo {
id: 2,
label: "Profit".to_string(),
completed: false,
status: Status::Pending,
},
],
}
@@ -49,17 +94,10 @@ pub fn App() -> impl IntoView {
let input_ref = NodeRef::new();
let rows = move || {
store
.todos()
.iter()
.enumerate()
.map(|(idx, todo)| view! { <TodoRow store idx todo/> })
.collect_view()
};
view! {
<p>"Hello, " {move || store.user().get()}</p>
<p>"Hello, " {move || store.user().name().get()}</p>
<UserForm user=store.user()/>
<hr/>
<form on:submit=move |ev| {
ev.prevent_default();
store.todos().write().push(Todo::new(input_ref.get().unwrap().value()));
@@ -67,30 +105,69 @@ pub fn App() -> impl IntoView {
<label>"Add a Todo" <input type="text" node_ref=input_ref/></label>
<input type="submit"/>
</form>
<ol>{rows}</ol>
<div style="display: flex"></div>
<ol>
// because `todos` is a keyed field, `store.todos()` returns a struct that
// directly implements IntoIterator, so we can use it in <For/> and
// it will manage reactivity for the store fields correctly
<For
each=move || {
leptos::logging::log!("RERUNNING FOR CALCULATION");
store.todos()
}
key=|row| row.id().get()
let:todo
>
<TodoRow store todo/>
</For>
</ol>
<pre>{move || serde_json::to_string_pretty(&*store.read())}</pre>
}
}
#[component]
fn UserForm(#[prop(into)] user: Field<User>) -> impl IntoView {
let error = RwSignal::new(None);
view! {
{move || error.get().map(|n| view! { <p>{n}</p> })}
<form on:submit:target=move |ev| {
ev.prevent_default();
match User::from_event(&ev) {
Ok(new_user) => {
error.set(None);
user.patch(new_user);
}
Err(e) => error.set(Some(e.to_string())),
}
}>
<label>
"Name" <input type="text" name="name" prop:value=move || user.name().get()/>
</label>
<label>
"Email" <input type="email" name="email" prop:value=move || user.email().get()/>
</label>
<input type="submit"/>
</form>
}
}
#[component]
fn TodoRow(
store: Store<Todos>,
idx: usize,
#[prop(into)] todo: Field<Todo>,
) -> impl IntoView {
let completed = todo.completed();
let status = todo.status();
let title = todo.label();
let editing = RwSignal::new(false);
let editing = RwSignal::new(true);
view! {
<li
style:text-decoration=move || {
completed.get().then_some("line-through").unwrap_or_default()
}
<li style:text-decoration=move || {
status.done().then_some("line-through").unwrap_or_default()
}>
class:foo=move || completed.get()
>
<p
class:hidden=move || editing.get()
on:click=move |_| {
@@ -106,25 +183,48 @@ fn TodoRow(
prop:value=move || title.get()
on:change=move |ev| {
title.set(event_target_value(&ev));
editing.set(false);
}
on:blur=move |_| editing.set(false)
autofocus
/>
<input
type="checkbox"
prop:checked=move || completed.get()
on:click=move |_| { completed.update(|n| *n = !*n) }
/>
<button on:click=move |_| {
store
.todos()
.update(|todos| {
todos.remove(idx);
});
status.write().next_step()
}>
{move || {
if todo.status().done() {
"Done"
} else if status.scheduled() || status.scheduled_for() {
"Scheduled"
} else {
"Pending"
}
}}
</button>
<button on:click=move |_| {
let id = todo.id().get();
store.todos().write().retain(|todo| todo.id != id);
}>"X"</button>
<input
type="date"
prop:value=move || {
todo.status().scheduled_for_date().map(|n| n.get().to_string())
}
class:hidden=move || !todo.status().scheduled_for()
on:change:target=move |ev| {
if let Some(date) = todo.status().scheduled_for_date() {
let value = ev.target().value();
match NaiveDate::parse_from_str(&value, "%Y-%m-%d") {
Ok(new_date) => {
date.set(new_date);
}
Err(e) => warn!("{e}"),
}
}
}
/>
</li>
}
}

View File

@@ -147,14 +147,13 @@ fn Nested() -> impl IntoView {
"Loading 1..."
}>
{move || {
one_second.get().map(|_| view! { <p id="loaded-1">"One Second: Loaded 1!"</p> })
one_second.map(|_| view! { <p id="loaded-1">"One Second: Loaded 1!"</p> })
}}
<Suspense fallback=|| {
"Loading 2..."
}>
{move || {
two_second
.get()
.map(|_| {
view! {
<p id="loaded-2">"Two Second: Loaded 2!"</p>
@@ -217,7 +216,6 @@ fn Parallel() -> impl IntoView {
}>
{move || {
one_second
.get()
.map(move |_| {
view! {
<p id="loaded-1">"One Second: Loaded 1!"</p>
@@ -234,7 +232,6 @@ fn Parallel() -> impl IntoView {
}>
{move || {
two_second
.get()
.map(move |_| {
view! {
<p id="loaded-2">"Two Second: Loaded 2!"</p>
@@ -264,7 +261,7 @@ fn Single() -> impl IntoView {
"Loading 1..."
}>
{move || {
one_second.get().map(|_| view! { <p id="loaded-1">"One Second: Loaded 1!"</p> })
one_second.map(|_| view! { <p id="loaded-1">"One Second: Loaded 1!"</p> })
}}
</Suspense>
@@ -300,7 +297,7 @@ fn InsideComponentChild() -> impl IntoView {
"Loading 1..."
}>
{move || {
one_second.get().map(|_| view! { <p id="loaded-1">"One Second: Loaded 1!"</p> })
one_second.map(|_| view! { <p id="loaded-1">"One Second: Loaded 1!"</p> })
}}
</Suspense>
@@ -319,7 +316,7 @@ fn LocalResource() -> impl IntoView {
"Loading 1..."
}>
{move || {
one_second.get().map(|_| view! { <p id="loaded-1">"One Second: Loaded 1!"</p> })
one_second.map(|_| view! { <p id="loaded-1">"One Second: Loaded 1!"</p> })
}}
{move || {
Suspend::new(async move {

View File

@@ -319,10 +319,7 @@ pub fn Todo(todo: Todo) -> impl IntoView {
node_ref=todo_input
class="toggle"
type="checkbox"
prop:checked=move || todo.completed.get()
on:input:target=move |ev| {
todo.completed.set(ev.target().checked());
}
bind:checked=todo.completed
/>
<label on:dblclick=move |_| {

View File

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

View File

@@ -27,6 +27,7 @@ type SealedErrors = Arc<RwLock<HashSet<SerializedDataId>>>;
/// The shared context that should be used on the server side.
pub struct SsrSharedContext {
id: AtomicUsize,
non_hydration_id: AtomicUsize,
is_hydrating: AtomicBool,
sync_buf: RwLock<Vec<ResolvedData>>,
async_buf: AsyncDataBuf,
@@ -41,6 +42,7 @@ impl SsrSharedContext {
pub fn new() -> Self {
Self {
is_hydrating: AtomicBool::new(true),
non_hydration_id: AtomicUsize::new(usize::MAX),
..Default::default()
}
}
@@ -52,6 +54,7 @@ impl SsrSharedContext {
pub fn new_islands() -> Self {
Self {
is_hydrating: AtomicBool::new(false),
non_hydration_id: AtomicUsize::new(usize::MAX),
..Default::default()
}
}
@@ -73,8 +76,13 @@ impl SharedContext for SsrSharedContext {
false
}
#[track_caller]
fn next_id(&self) -> SerializedDataId {
let id = self.id.fetch_add(1, Ordering::Relaxed);
let id = if self.get_is_hydrating() {
self.id.fetch_add(1, Ordering::Relaxed)
} else {
self.non_hydration_id.fetch_sub(1, Ordering::Relaxed)
};
SerializedDataId(id)
}

View File

@@ -10,6 +10,7 @@ edition.workspace = true
[dependencies]
actix-http = "3.8"
actix-files = "0.6"
actix-web = "4.8"
futures = "0.3.30"
any_spawner = { workspace = true, features = ["tokio"] }
@@ -25,12 +26,14 @@ parking_lot = "0.12.3"
tracing = { version = "0.1", optional = true }
tokio = { version = "1.39", features = ["rt", "fs"] }
send_wrapper = "0.6.0"
dashmap = "6"
once_cell = "1"
[package.metadata.docs.rs]
rustdoc-args = ["--generate-link-to-definition"]
[features]
islands-router = []
dont-use-islands-router = []
tracing = ["dep:tracing"]
[package.metadata.cargo-all-features]

View File

@@ -6,30 +6,38 @@
//! [`examples`](https://github.com/leptos-rs/leptos/tree/main/examples)
//! directory in the Leptos repository.
use actix_files::NamedFile;
use actix_http::header::{HeaderName, HeaderValue, ACCEPT, LOCATION, REFERER};
use actix_web::{
body::BoxBody,
dev::{ServiceFactory, ServiceRequest},
http::header,
web::{Payload, ServiceConfig},
test,
web::{Data, Payload, ServiceConfig},
*,
};
use dashmap::DashMap;
use futures::{stream::once, Stream, StreamExt};
use http::StatusCode;
use hydration_context::SsrSharedContext;
use leptos::{
config::LeptosOptions,
context::{provide_context, use_context},
reactive_graph::{computed::ScopedFuture, owner::Owner},
IntoView, *,
prelude::expect_context,
reactive::{computed::ScopedFuture, owner::Owner},
IntoView,
};
use leptos_integration_utils::{
BoxedFnOnce, ExtendResponse, PinnedFuture, PinnedStream,
};
use leptos_meta::ServerMetaContext;
use leptos_router::{
components::provide_server_redirect, location::RequestUrl, PathSegment,
RouteList, RouteListing, SsrMode, StaticDataMap, StaticMode, *,
components::provide_server_redirect,
location::RequestUrl,
static_routes::{RegenerationFn, ResolvedStaticPath},
Method, PathSegment, RouteList, RouteListing, SsrMode,
};
use once_cell::sync::Lazy;
use parking_lot::RwLock;
use send_wrapper::SendWrapper;
use server_fn::{
@@ -37,7 +45,9 @@ use server_fn::{
};
use std::{
fmt::{Debug, Display},
future::Future,
ops::{Deref, DerefMut},
path::Path,
sync::Arc,
};
@@ -728,13 +738,25 @@ pub fn render_app_async_with_context<IV>(
where
IV: IntoView + 'static,
{
handle_response(method, additional_context, app_fn, |app, chunks| {
Box::pin(async move {
let app = app.to_html_stream_in_order().collect::<String>().await;
let chunks = chunks();
Box::pin(once(async move { app }).chain(chunks))
as PinnedStream<String>
})
handle_response(method, additional_context, app_fn, async_stream_builder)
}
fn async_stream_builder<IV>(
app: IV,
chunks: BoxedFnOnce<PinnedStream<String>>,
) -> PinnedFuture<PinnedStream<String>>
where
IV: IntoView + 'static,
{
Box::pin(async move {
let app = if cfg!(feature = "dont-use-islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
};
let app = app.collect::<String>().await;
let chunks = chunks();
Box::pin(once(async move { app }).chain(chunks)) as PinnedStream<String>
})
}
@@ -822,7 +844,7 @@ where
/// create routes in Actix's App without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths.
pub fn generate_route_list<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
app_fn: impl Fn() -> IV + 'static + Send + Clone,
) -> Vec<ActixRouteListing>
where
IV: IntoView + 'static,
@@ -834,8 +856,8 @@ where
/// create routes in Actix's App without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths.
pub fn generate_route_list_with_ssg<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
) -> (Vec<ActixRouteListing>, StaticDataMap)
app_fn: impl Fn() -> IV + 'static + Send + Clone,
) -> (Vec<ActixRouteListing>, StaticRouteGenerator)
where
IV: IntoView + 'static,
{
@@ -847,7 +869,7 @@ where
/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths. Adding excluded_routes
/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Actix path format
pub fn generate_route_list_with_exclusions<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
app_fn: impl Fn() -> IV + 'static + Send + Clone,
excluded_routes: Option<Vec<String>>,
) -> Vec<ActixRouteListing>
where
@@ -861,9 +883,9 @@ where
/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths. Adding excluded_routes
/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Actix path format
pub fn generate_route_list_with_exclusions_and_ssg<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
app_fn: impl Fn() -> IV + 'static + Send + Clone,
excluded_routes: Option<Vec<String>>,
) -> (Vec<ActixRouteListing>, StaticDataMap)
) -> (Vec<ActixRouteListing>, StaticRouteGenerator)
where
IV: IntoView + 'static,
{
@@ -912,7 +934,7 @@ pub struct ActixRouteListing {
path: String,
mode: SsrMode,
methods: Vec<leptos_router::Method>,
static_mode: Option<(StaticMode, StaticDataMap)>,
regenerate: Vec<RegenerationFn>,
}
impl From<RouteListing> for ActixRouteListing {
@@ -925,12 +947,12 @@ impl From<RouteListing> for ActixRouteListing {
};
let mode = value.mode();
let methods = value.methods().collect();
let static_mode = value.into_static_parts();
let regenerate = value.regenerate().into();
Self {
path,
mode,
mode: mode.clone(),
methods,
static_mode,
regenerate,
}
}
}
@@ -941,13 +963,13 @@ impl ActixRouteListing {
path: String,
mode: SsrMode,
methods: impl IntoIterator<Item = leptos_router::Method>,
static_mode: Option<(StaticMode, StaticDataMap)>,
regenerate: impl Into<Vec<RegenerationFn>>,
) -> Self {
Self {
path,
mode,
methods: methods.into_iter().collect(),
static_mode,
regenerate: regenerate.into(),
}
}
@@ -958,19 +980,13 @@ impl ActixRouteListing {
/// The rendering mode for this path.
pub fn mode(&self) -> SsrMode {
self.mode
self.mode.clone()
}
/// The HTTP request methods this path can handle.
pub fn methods(&self) -> impl Iterator<Item = leptos_router::Method> + '_ {
self.methods.iter().copied()
}
/// Whether this route is statically rendered.
#[inline(always)]
pub fn static_mode(&self) -> Option<StaticMode> {
self.static_mode.as_ref().map(|n| n.0)
}
}
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
@@ -979,10 +995,10 @@ impl ActixRouteListing {
/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Actix path format.
/// Additional context will be provided to the app Element.
pub fn generate_route_list_with_exclusions_and_ssg_and_context<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
app_fn: impl Fn() -> IV + 'static + Send + Clone,
excluded_routes: Option<Vec<String>>,
additional_context: impl Fn() + 'static + Clone,
) -> (Vec<ActixRouteListing>, StaticDataMap)
additional_context: impl Fn() + 'static + Send + Clone,
) -> (Vec<ActixRouteListing>, StaticRouteGenerator)
where
IV: IntoView + 'static,
{
@@ -1001,6 +1017,12 @@ where
})
.unwrap_or_default();
let generator = StaticRouteGenerator::new(
&routes,
app_fn.clone(),
additional_context.clone(),
);
// Axum's Router defines Root routes as "/" not ""
let mut routes = routes
.into_inner()
@@ -1014,7 +1036,7 @@ where
"/".to_string(),
Default::default(),
[leptos_router::Method::Get],
None,
vec![],
)]
} else {
// Routes to exclude from auto generation
@@ -1024,192 +1046,251 @@ where
}
routes
},
StaticDataMap::new(), // TODO
//static_data_map,
generator,
)
}
/// Allows generating any prerendered routes.
#[allow(clippy::type_complexity)]
pub struct StaticRouteGenerator(
Box<dyn FnOnce(&LeptosOptions) -> PinnedFuture<()> + Send>,
);
impl StaticRouteGenerator {
fn render_route<IV: IntoView + 'static>(
path: String,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + Clone + Send + 'static,
) -> impl Future<Output = (Owner, String)> {
let (meta_context, meta_output) = ServerMetaContext::new();
let additional_context = {
let add_context = additional_context.clone();
move || {
let mock_req = test::TestRequest::with_uri(&path)
.insert_header(("Accept", "text/html"))
.to_http_request();
let res_options = ResponseOptions::default();
provide_contexts(
Request::new(&mock_req),
&meta_context,
&res_options,
);
add_context();
}
};
let (owner, stream) = leptos_integration_utils::build_response(
app_fn.clone(),
additional_context,
async_stream_builder,
);
let sc = owner.shared_context().unwrap();
async move {
let stream = stream.await;
while let Some(pending) = sc.await_deferred() {
pending.await;
}
let html = meta_output
.inject_meta_context(stream)
.await
.collect::<String>()
.await;
(owner, html)
}
}
/// Creates a new static route generator from the given list of route definitions.
pub fn new<IV>(
routes: &RouteList,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static,
{
Self({
let routes = routes.clone();
Box::new(move |options| {
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
Box::pin(routes.generate_static_files(
move |path: &ResolvedStaticPath| {
Self::render_route(
path.to_string(),
app_fn.clone(),
additional_context.clone(),
)
},
move |path: &ResolvedStaticPath,
owner: &Owner,
html: String| {
let options = options.clone();
let path = path.to_owned();
let response_options = owner.with(use_context);
async move {
write_static_route(
&options,
response_options,
path.as_ref(),
&html,
)
.await
}
},
was_404,
))
})
})
}
/// Generates the routes.
pub async fn generate(self, options: &LeptosOptions) {
(self.0)(options).await
}
}
static STATIC_HEADERS: Lazy<DashMap<String, ResponseOptions>> =
Lazy::new(DashMap::new);
fn was_404(owner: &Owner) -> bool {
let resp = owner.with(|| expect_context::<ResponseOptions>());
let status = resp.0.read().status;
if let Some(status) = status {
return status == StatusCode::NOT_FOUND;
}
false
}
fn static_path(options: &LeptosOptions, path: &str) -> String {
use leptos_integration_utils::static_file_path;
// If the path ends with a trailing slash, we generate the path
// as a directory with a index.html file inside.
if path != "/" && path.ends_with("/") {
static_file_path(options, &format!("{}index", path))
} else {
static_file_path(options, path)
}
}
async fn write_static_route(
options: &LeptosOptions,
response_options: Option<ResponseOptions>,
path: &str,
html: &str,
) -> Result<(), std::io::Error> {
if let Some(options) = response_options {
STATIC_HEADERS.insert(path.to_string(), options);
}
let path = static_path(options, path);
let path = Path::new(&path);
if let Some(path) = path.parent() {
tokio::fs::create_dir_all(path).await?;
}
tokio::fs::write(path, &html).await?;
Ok(())
}
fn handle_static_route<IV>(
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
regenerate: Vec<RegenerationFn>,
) -> Route
where
IV: IntoView + 'static,
{
let handler = move |req: HttpRequest, data: Data<LeptosOptions>| {
Box::pin({
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
let regenerate = regenerate.clone();
async move {
let options = data.into_inner();
let orig_path = req.uri().path();
let path = static_path(&options, orig_path);
let path = Path::new(&path);
let exists = tokio::fs::try_exists(path).await.unwrap_or(false);
let (response_options, html) = if !exists {
let path = ResolvedStaticPath::new(orig_path);
let (owner, html) = path
.build(
move |path: &ResolvedStaticPath| {
StaticRouteGenerator::render_route(
path.to_string(),
app_fn.clone(),
additional_context.clone(),
)
},
move |path: &ResolvedStaticPath,
owner: &Owner,
html: String| {
let options = options.clone();
let path = path.to_owned();
let response_options = owner.with(use_context);
async move {
write_static_route(
&options,
response_options,
path.as_ref(),
&html,
)
.await
}
},
was_404,
regenerate,
)
.await;
(owner.with(use_context::<ResponseOptions>), html)
} else {
let headers =
STATIC_HEADERS.get(orig_path).map(|v| v.clone());
(headers, None)
};
// if html is Some(_), it means that `was_error_response` is true and we're not
// actually going to cache this route, just return it as HTML
//
// this if for thing like 404s, where we do not want to cache an endless series of
// typos (or malicious requests)
let mut res = ActixResponse(match html {
Some(html) => {
HttpResponse::Ok().content_type("text/html").body(html)
}
None => match NamedFile::open(path) {
Ok(res) => res.into_response(&req),
Err(err) => HttpResponse::InternalServerError()
.body(err.to_string()),
},
});
if let Some(options) = response_options {
res.extend_response(&options);
}
res.0
}
})
};
web::get().to(handler)
}
pub enum DataResponse<T> {
Data(T),
Response(actix_web::dev::Response<BoxBody>),
}
// TODO static response
/*
fn handle_static_response<'a, IV>(
path: &'a str,
options: &'a LeptosOptions,
app_fn: &'a (impl Fn() -> IV + Clone + Send + 'static),
additional_context: &'a (impl Fn() + 'static + Clone + Send),
res: StaticResponse,
) -> Pin<Box<dyn Future<Output = HttpResponse<String>> + 'a>>
where
IV: IntoView + 'static,
{
Box::pin(async move {
match res {
StaticResponse::ReturnResponse {
body,
status,
content_type,
} => {
let mut res = HttpResponse::new(match status {
StaticStatusCode::Ok => StatusCode::OK,
StaticStatusCode::NotFound => StatusCode::NOT_FOUND,
StaticStatusCode::InternalServerError => {
StatusCode::INTERNAL_SERVER_ERROR
}
});
if let Some(v) = content_type {
res.headers_mut().insert(
HeaderName::from_static("content-type"),
HeaderValue::from_static(v),
);
}
res.set_body(body)
}
StaticResponse::RenderDynamic => {
handle_static_response(
path,
options,
app_fn,
additional_context,
render_dynamic(
path,
options,
app_fn.clone(),
additional_context.clone(),
)
.await,
)
.await
}
StaticResponse::RenderNotFound => {
handle_static_response(
path,
options,
app_fn,
additional_context,
not_found_page(
tokio::fs::read_to_string(not_found_path(options))
.await,
),
)
.await
}
StaticResponse::WriteFile { body, path } => {
if let Some(path) = path.parent() {
if let Err(e) = std::fs::create_dir_all(path) {
tracing::error!(
"encountered error {} writing directories {}",
e,
path.display()
);
}
}
if let Err(e) = std::fs::write(&path, &body) {
tracing::error!(
"encountered error {} writing file {}",
e,
path.display()
);
}
handle_static_response(
path.to_str().unwrap(),
options,
app_fn,
additional_context,
StaticResponse::ReturnResponse {
body,
status: StaticStatusCode::Ok,
content_type: Some("text/html"),
},
)
.await
}
}
})
}
fn static_route<IV>(
options: LeptosOptions,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + 'static + Clone + Send,
method: Method,
mode: StaticMode,
) -> Route
where
IV: IntoView + 'static,
{
match mode {
StaticMode::Incremental => {
let handler = move |req: HttpRequest| {
Box::pin({
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
async move {
handle_static_response(
req.path(),
&options,
&app_fn,
&additional_context,
incremental_static_route(
tokio::fs::read_to_string(static_file_path(
&options,
req.path(),
))
.await,
),
)
.await
}
})
};
match method {
Method::Get => web::get().to(handler),
Method::Post => web::post().to(handler),
Method::Put => web::put().to(handler),
Method::Delete => web::delete().to(handler),
Method::Patch => web::patch().to(handler),
}
}
StaticMode::Upfront => {
let handler = move |req: HttpRequest| {
Box::pin({
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
async move {
handle_static_response(
req.path(),
&options,
&app_fn,
&additional_context,
upfront_static_route(
tokio::fs::read_to_string(static_file_path(
&options,
req.path(),
))
.await,
),
)
.await
}
})
};
match method {
Method::Get => web::get().to(handler),
Method::Post => web::post().to(handler),
Method::Put => web::put().to(handler),
Method::Delete => web::delete().to(handler),
Method::Patch => web::patch().to(handler),
}
}
}
}
*/
/// This trait allows one to pass a list of routes and a render function to Actix's router, letting us avoid
/// having to use wildcards or manually define all routes in multiple places.
pub trait LeptosRoutes {
@@ -1290,19 +1371,15 @@ where
provide_context(method);
additional_context();
};
router = if let Some(static_mode) = listing.static_mode() {
_ = static_mode;
todo!() /*
router.route(
path,
static_route(
app_fn.clone(),
additional_context_and_method.clone(),
method,
static_mode,
),
)
*/
router = if matches!(listing.mode(), SsrMode::Static(_)) {
router.route(
path,
handle_static_route(
additional_context_and_method.clone(),
app_fn.clone(),
listing.regenerate.clone(),
),
)
} else {
router.route(
path,
@@ -1334,6 +1411,7 @@ where
app_fn.clone(),
method,
),
_ => unreachable!()
},
)
};
@@ -1390,7 +1468,17 @@ impl LeptosRoutes for &mut ServiceConfig {
let mode = listing.mode();
for method in listing.methods() {
router = router.route(
if matches!(listing.mode(), SsrMode::Static(_)) {
router = router.route(
path,
handle_static_route(
additional_context.clone(),
app_fn.clone(),
listing.regenerate.clone(),
),
)
} else {
router = router.route(
path,
match mode {
SsrMode::OutOfOrder => {
@@ -1420,8 +1508,10 @@ impl LeptosRoutes for &mut ServiceConfig {
app_fn.clone(),
method,
),
_ => unreachable!()
},
);
}
}
}

View File

@@ -14,6 +14,7 @@ hydration_context = { workspace = true }
axum = { version = "0.7.5", default-features = false, features = [
"matched-path",
] }
dashmap = "6"
futures = "0.3.30"
http = "1.1"
http-body-util = "0.1.2"
@@ -23,6 +24,7 @@ leptos_macro = { workspace = true, features = ["axum"] }
leptos_meta = { workspace = true, features = ["ssr"] }
leptos_router = { workspace = true, features = ["ssr"] }
leptos_integration_utils = { workspace = true }
once_cell = "1"
parking_lot = "0.12.3"
serde_json = "1.0"
tokio = { version = "1.39", default-features = false }
@@ -36,8 +38,8 @@ tokio = { version = "1.39", features = ["net", "rt-multi-thread"] }
[features]
wasm = []
default = ["tokio/fs", "tokio/sync", "tower-http/fs"]
islands-router = []
default = ["tokio/fs", "tokio/sync", "tower-http/fs", "tower/util"]
dont-use-islands-router = []
tracing = ["dep:tracing"]
[package.metadata.docs.rs]

View File

@@ -32,9 +32,11 @@
//! [`examples`](https://github.com/leptos-rs/leptos/tree/main/examples)
//! directory in the Leptos repository.
#[cfg(feature = "default")]
use axum::http::Uri;
use axum::{
body::{Body, Bytes},
extract::{FromRequestParts, MatchedPath},
extract::{FromRef, FromRequestParts, MatchedPath, State},
http::{
header::{self, HeaderName, HeaderValue, ACCEPT, LOCATION, REFERER},
request::Parts,
@@ -44,29 +46,34 @@ use axum::{
routing::{delete, get, patch, post, put},
};
#[cfg(feature = "default")]
use axum::{
extract::{FromRef, State},
http::Uri,
};
use dashmap::DashMap;
use futures::{stream::once, Future, Stream, StreamExt};
use hydration_context::SsrSharedContext;
use leptos::{
config::LeptosOptions,
context::{provide_context, use_context},
prelude::*,
reactive_graph::{computed::ScopedFuture, owner::Owner},
reactive::{computed::ScopedFuture, owner::Owner},
IntoView,
};
use leptos_integration_utils::{
BoxedFnOnce, ExtendResponse, PinnedFuture, PinnedStream,
};
use leptos_meta::ServerMetaContext;
#[cfg(feature = "default")]
use leptos_router::static_routes::ResolvedStaticPath;
use leptos_router::{
components::provide_server_redirect, location::RequestUrl, PathSegment,
RouteList, RouteListing, SsrMode, StaticDataMap, StaticMode,
components::provide_server_redirect,
location::RequestUrl,
static_routes::{RegenerationFn, StaticParamsMap},
PathSegment, RouteList, RouteListing, SsrMode,
};
#[cfg(feature = "default")]
use once_cell::sync::Lazy;
use parking_lot::RwLock;
use server_fn::{redirect::REDIRECT_HEADER, ServerFnError};
#[cfg(feature = "default")]
use std::path::Path;
use std::{fmt::Debug, io, pin::Pin, sync::Arc};
#[cfg(feature = "default")]
use tower::ServiceExt;
@@ -236,14 +243,20 @@ pub fn redirect(path: &str) {
);
}
} else {
let msg = "Couldn't retrieve either Parts or ResponseOptions while \
trying to redirect().";
#[cfg(feature = "tracing")]
tracing::warn!("{}", &msg);
{
tracing::warn!(
"Couldn't retrieve either Parts or ResponseOptions while \
trying to redirect()."
);
}
#[cfg(not(feature = "tracing"))]
eprintln!("{}", &msg);
{
eprintln!(
"Couldn't retrieve either Parts or ResponseOptions while \
trying to redirect()."
);
}
}
}
@@ -497,10 +510,11 @@ where
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_route<IV>(
pub fn render_route<S, IV>(
paths: Vec<AxumRouteListing>,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> impl Fn(
State<S>,
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
+ Clone
@@ -508,6 +522,8 @@ pub fn render_route<IV>(
+ 'static
where
IV: IntoView + 'static,
LeptosOptions: FromRef<S>,
S: Send + 'static,
{
render_route_with_context(paths, || {}, app_fn)
}
@@ -648,11 +664,12 @@ where
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_route_with_context<IV>(
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,
) -> impl Fn(
State<S>,
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
+ Clone
@@ -660,6 +677,8 @@ pub fn render_route_with_context<IV>(
+ 'static
where
IV: IntoView + 'static,
LeptosOptions: FromRef<S>,
S: Send + 'static,
{
let ooo = render_app_to_stream_with_context(
additional_context.clone(),
@@ -679,7 +698,7 @@ where
app_fn.clone(),
);
move |req| {
move |state, req| {
// 1. Process route to match the values in routeListing
let path = req
.extensions()
@@ -702,6 +721,25 @@ where
SsrMode::PartiallyBlocked => pb(req),
SsrMode::InOrder => io(req),
SsrMode::Async => asyn(req),
SsrMode::Static(_) => {
#[cfg(feature = "default")]
{
let regenerate = listing.regenerate.clone();
handle_static_route(
additional_context.clone(),
app_fn.clone(),
regenerate,
)(state, req)
}
#[cfg(not(feature = "default"))]
{
_ = state;
panic!(
"Static routes are not currently supported on WASM32 \
server targets."
);
}
}
}
}
}
@@ -746,7 +784,7 @@ where
_ = replace_blocks; // TODO
handle_response(additional_context, app_fn, |app, chunks| {
Box::pin(async move {
let app = if cfg!(feature = "islands-router") {
let app = if cfg!(feature = "dont-use-islands-router") {
app.to_html_stream_out_of_order_branching()
} else {
app.to_html_stream_out_of_order()
@@ -811,7 +849,7 @@ where
IV: IntoView + 'static,
{
handle_response(additional_context, app_fn, |app, chunks| {
let app = if cfg!(feature = "islands-router") {
let app = if cfg!(feature = "dont-use-islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
@@ -1031,7 +1069,7 @@ where
{
handle_response(additional_context, app_fn, |app, chunks| {
Box::pin(async move {
let app = if cfg!(feature = "islands-router") {
let app = if cfg!(feature = "dont-use-islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
@@ -1097,18 +1135,25 @@ pub fn render_app_async_with_context<IV>(
where
IV: IntoView + 'static,
{
handle_response(additional_context, app_fn, |app, chunks| {
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()
};
let app = app.collect::<String>().await;
let chunks = chunks();
Box::pin(once(async move { app }).chain(chunks))
as PinnedStream<String>
})
handle_response(additional_context, app_fn, async_stream_builder)
}
fn async_stream_builder<IV>(
app: IV,
chunks: BoxedFnOnce<PinnedStream<String>>,
) -> PinnedFuture<PinnedStream<String>>
where
IV: IntoView + 'static,
{
Box::pin(async move {
let app = if cfg!(feature = "dont-use-islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
};
let app = app.collect::<String>().await;
let chunks = chunks();
Box::pin(once(async move { app }).chain(chunks)) as PinnedStream<String>
})
}
@@ -1120,7 +1165,7 @@ where
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn generate_route_list<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
app_fn: impl Fn() -> IV + 'static + Clone + Send,
) -> Vec<AxumRouteListing>
where
IV: IntoView + 'static,
@@ -1128,7 +1173,7 @@ where
generate_route_list_with_exclusions_and_ssg(app_fn, None).0
}
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use t.clone()his to automatically
/// create routes in Axum's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths.
#[cfg_attr(
@@ -1136,8 +1181,8 @@ where
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn generate_route_list_with_ssg<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
) -> (Vec<AxumRouteListing>, StaticDataMap)
app_fn: impl Fn() -> IV + 'static + Clone + Send,
) -> (Vec<AxumRouteListing>, StaticRouteGenerator)
where
IV: IntoView + 'static,
{
@@ -1153,7 +1198,7 @@ where
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn generate_route_list_with_exclusions<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
app_fn: impl Fn() -> IV + 'static + Clone + Send,
excluded_routes: Option<Vec<String>>,
) -> Vec<AxumRouteListing>
where
@@ -1162,13 +1207,13 @@ where
generate_route_list_with_exclusions_and_ssg(app_fn, excluded_routes).0
}
/// TODO docs
/// Builds all routes that have been defined using [`StaticRoute`].
#[allow(unused)]
pub async fn build_static_routes<IV>(
options: &LeptosOptions,
app_fn: impl Fn() -> IV + 'static + Send + Clone,
routes: &[RouteListing],
static_data_map: StaticDataMap,
static_data_map: StaticParamsMap,
) where
IV: IntoView + 'static,
{
@@ -1197,9 +1242,9 @@ pub async fn build_static_routes<IV>(
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn generate_route_list_with_exclusions_and_ssg<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
app_fn: impl Fn() -> IV + 'static + Clone + Send,
excluded_routes: Option<Vec<String>>,
) -> (Vec<AxumRouteListing>, StaticDataMap)
) -> (Vec<AxumRouteListing>, StaticRouteGenerator)
where
IV: IntoView + 'static,
{
@@ -1216,7 +1261,8 @@ pub struct AxumRouteListing {
path: String,
mode: SsrMode,
methods: Vec<leptos_router::Method>,
static_mode: Option<(StaticMode, StaticDataMap)>,
#[allow(unused)]
regenerate: Vec<RegenerationFn>,
}
impl From<RouteListing> for AxumRouteListing {
@@ -1229,12 +1275,12 @@ impl From<RouteListing> for AxumRouteListing {
};
let mode = value.mode();
let methods = value.methods().collect();
let static_mode = value.into_static_parts();
let regenerate = value.regenerate().into();
Self {
path,
mode,
mode: mode.clone(),
methods,
static_mode,
regenerate,
}
}
}
@@ -1245,13 +1291,13 @@ impl AxumRouteListing {
path: String,
mode: SsrMode,
methods: impl IntoIterator<Item = leptos_router::Method>,
static_mode: Option<(StaticMode, StaticDataMap)>,
regenerate: impl Into<Vec<RegenerationFn>>,
) -> Self {
Self {
path,
mode,
methods: methods.into_iter().collect(),
static_mode,
regenerate: regenerate.into(),
}
}
@@ -1261,20 +1307,14 @@ impl AxumRouteListing {
}
/// The rendering mode for this path.
pub fn mode(&self) -> SsrMode {
self.mode
pub fn mode(&self) -> &SsrMode {
&self.mode
}
/// The HTTP request methods this path can handle.
pub fn methods(&self) -> impl Iterator<Item = leptos_router::Method> + '_ {
self.methods.iter().copied()
}
/// Whether this route is statically rendered.
#[inline(always)]
pub fn static_mode(&self) -> Option<StaticMode> {
self.static_mode.as_ref().map(|n| n.0)
}
}
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
@@ -1287,16 +1327,17 @@ impl AxumRouteListing {
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn generate_route_list_with_exclusions_and_ssg_and_context<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
excluded_routes: Option<Vec<String>>,
additional_context: impl Fn() + 'static + Clone,
) -> (Vec<AxumRouteListing>, StaticDataMap)
additional_context: impl Fn() + Clone + Send + 'static,
) -> (Vec<AxumRouteListing>, StaticRouteGenerator)
where
IV: IntoView + 'static,
{
// do some basic reactive setup
init_executor();
let owner = Owner::new_root(Some(Arc::new(SsrSharedContext::new())));
let routes = owner
.with(|| {
// stub out a path for now
@@ -1310,6 +1351,12 @@ where
})
.unwrap_or_default();
let generator = StaticRouteGenerator::new(
&routes,
app_fn.clone(),
additional_context.clone(),
);
// Axum's Router defines Root routes as "/" not ""
let mut routes = routes
.into_inner()
@@ -1323,7 +1370,7 @@ where
"/".to_string(),
Default::default(),
[leptos_router::Method::Get],
None,
vec![],
)]
} else {
// Routes to exclude from auto generation
@@ -1333,16 +1380,284 @@ where
}
routes
},
StaticDataMap::new(), // TODO
//static_data_map,
generator,
)
}
/// Allows generating any prerendered routes.
#[allow(clippy::type_complexity)]
pub struct StaticRouteGenerator(
Box<dyn FnOnce(&LeptosOptions) -> PinnedFuture<()> + Send>,
);
impl StaticRouteGenerator {
#[cfg(feature = "default")]
fn render_route<IV: IntoView + 'static>(
path: String,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + Clone + Send + 'static,
) -> impl Future<Output = (Owner, String)> {
let (meta_context, meta_output) = ServerMetaContext::new();
let additional_context = {
let add_context = additional_context.clone();
move || {
let full_path = format!("http://leptos.dev{path}");
let mock_req = http::Request::builder()
.method(http::Method::GET)
.header("Accept", "text/html")
.body(Body::empty())
.unwrap();
let (mock_parts, _) = mock_req.into_parts();
let res_options = ResponseOptions::default();
provide_contexts(
&full_path,
&meta_context,
mock_parts,
res_options,
);
add_context();
}
};
let (owner, stream) = leptos_integration_utils::build_response(
app_fn.clone(),
additional_context,
async_stream_builder,
);
let sc = owner.shared_context().unwrap();
async move {
let stream = stream.await;
while let Some(pending) = sc.await_deferred() {
pending.await;
}
let html = meta_output
.inject_meta_context(stream)
.await
.collect::<String>()
.await;
(owner, html)
}
}
/// Creates a new static route generator from the given list of route definitions.
pub fn new<IV>(
routes: &RouteList,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static,
{
#[cfg(feature = "default")]
{
Self({
let routes = routes.clone();
Box::new(move |options| {
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
Box::pin(routes.generate_static_files(
move |path: &ResolvedStaticPath| {
Self::render_route(
path.to_string(),
app_fn.clone(),
additional_context.clone(),
)
},
move |path: &ResolvedStaticPath,
owner: &Owner,
html: String| {
let options = options.clone();
let path = path.to_owned();
let response_options = owner.with(use_context);
async move {
write_static_route(
&options,
response_options,
path.as_ref(),
&html,
)
.await
}
},
was_404,
))
})
})
}
#[cfg(not(feature = "default"))]
{
_ = routes;
_ = app_fn;
_ = additional_context;
panic!(
"Static routes are not currently supported on WASM32 server \
targets."
);
}
}
/// Generates the routes.
pub async fn generate(self, options: &LeptosOptions) {
(self.0)(options).await
}
}
#[cfg(feature = "default")]
static STATIC_HEADERS: Lazy<DashMap<String, ResponseOptions>> =
Lazy::new(DashMap::new);
#[cfg(feature = "default")]
fn was_404(owner: &Owner) -> bool {
let resp = owner.with(|| expect_context::<ResponseOptions>());
let status = resp.0.read().status;
if let Some(status) = status {
return status == StatusCode::NOT_FOUND;
}
false
}
#[cfg(feature = "default")]
fn static_path(options: &LeptosOptions, path: &str) -> String {
use leptos_integration_utils::static_file_path;
// If the path ends with a trailing slash, we generate the path
// as a directory with a index.html file inside.
if path != "/" && path.ends_with("/") {
static_file_path(options, &format!("{}index", path))
} else {
static_file_path(options, path)
}
}
#[cfg(feature = "default")]
async fn write_static_route(
options: &LeptosOptions,
response_options: Option<ResponseOptions>,
path: &str,
html: &str,
) -> Result<(), std::io::Error> {
if let Some(options) = response_options {
STATIC_HEADERS.insert(path.to_string(), options);
}
let path = static_path(options, path);
let path = Path::new(&path);
if let Some(path) = path.parent() {
tokio::fs::create_dir_all(path).await?;
}
tokio::fs::write(path, &html).await?;
Ok(())
}
#[cfg(feature = "default")]
fn handle_static_route<S, IV>(
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
regenerate: Vec<RegenerationFn>,
) -> impl Fn(
State<S>,
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
+ Clone
+ Send
+ 'static
where
LeptosOptions: FromRef<S>,
S: Send + 'static,
IV: IntoView + 'static,
{
use tower_http::services::ServeFile;
move |state, req| {
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
let regenerate = regenerate.clone();
Box::pin(async move {
let options = LeptosOptions::from_ref(&state);
let orig_path = req.uri().path();
let path = static_path(&options, orig_path);
let path = Path::new(&path);
let exists = tokio::fs::try_exists(path).await.unwrap_or(false);
let (response_options, html) = if !exists {
let path = ResolvedStaticPath::new(orig_path);
let (owner, html) = path
.build(
move |path: &ResolvedStaticPath| {
StaticRouteGenerator::render_route(
path.to_string(),
app_fn.clone(),
additional_context.clone(),
)
},
move |path: &ResolvedStaticPath,
owner: &Owner,
html: String| {
let options = options.clone();
let path = path.to_owned();
let response_options = owner.with(use_context);
async move {
write_static_route(
&options,
response_options,
path.as_ref(),
&html,
)
.await
}
},
was_404,
regenerate,
)
.await;
(owner.with(use_context::<ResponseOptions>), html)
} else {
let headers = STATIC_HEADERS.get(orig_path).map(|v| v.clone());
(headers, None)
};
// if html is Some(_), it means that `was_error_response` is true and we're not
// actually going to cache this route, just return it as HTML
//
// this if for thing like 404s, where we do not want to cache an endless series of
// typos (or malicious requests)
let mut res = AxumResponse(match html {
Some(html) => axum::response::Html(html).into_response(),
None => match ServeFile::new(path).oneshot(req).await {
Ok(res) => res.into_response(),
Err(err) => (
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {err}"),
)
.into_response(),
},
});
if let Some(options) = response_options {
res.extend_response(&options);
}
res.0
})
}
}
/// This trait allows one to pass a list of routes and a render function to Axum's router, letting us avoid
/// having to use wildcards or manually define all routes in multiple places.
pub trait LeptosRoutes<S>
where
S: Clone + Send + Sync + 'static,
LeptosOptions: FromRef<S>,
{
fn leptos_routes<IV>(
self,
@@ -1372,209 +1687,6 @@ where
H: axum::handler::Handler<T, S>,
T: 'static;
}
/*
#[cfg(feature = "default")]
fn handle_static_response<IV>(
path: String,
options: LeptosOptions,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + Clone + Send + 'static,
res: StaticResponse,
) -> Pin<Box<dyn Future<Output = Response<String>> + 'static>>
where
IV: IntoView + 'static,
{
Box::pin(async move {
match res {
StaticResponse::ReturnResponse {
body,
status,
content_type,
} => {
let mut res = Response::new(body);
if let Some(v) = content_type {
res.headers_mut().insert(
HeaderName::from_static("content-type"),
HeaderValue::from_static(v),
);
}
*res.status_mut() = match status {
StaticStatusCode::Ok => StatusCode::OK,
StaticStatusCode::NotFound => StatusCode::NOT_FOUND,
StaticStatusCode::InternalServerError => {
StatusCode::INTERNAL_SERVER_ERROR
}
};
res
}
StaticResponse::RenderDynamic => {
let res = render_dynamic(
&path,
&options,
app_fn.clone(),
additional_context.clone(),
)
.await;
handle_static_response(
path,
options,
app_fn,
additional_context,
res,
)
.await
}
StaticResponse::RenderNotFound => {
let res = not_found_page(
tokio::fs::read_to_string(not_found_path(&options)).await,
);
handle_static_response(
path,
options,
app_fn,
additional_context,
res,
)
.await
}
StaticResponse::WriteFile { body, path } => {
if let Some(path) = path.parent() {
if let Err(e) = std::fs::create_dir_all(path) {
tracing::error!(
"encountered error {} writing directories {}",
e,
path.display()
);
}
}
if let Err(e) = std::fs::write(&path, &body) {
tracing::error!(
"encountered error {} writing file {}",
e,
path.display()
);
}
handle_static_response(
path.to_str().unwrap().to_string(),
options,
app_fn,
additional_context,
StaticResponse::ReturnResponse {
body,
status: StaticStatusCode::Ok,
content_type: Some("text/html"),
},
)
.await
}
}
})
}*/
#[allow(unused)] // TODO
#[cfg(feature = "default")]
fn static_route<IV, S>(
router: axum::Router<S>,
path: &str,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + Clone + Send + 'static,
method: leptos_router::Method,
mode: StaticMode,
) -> axum::Router<S>
where
IV: IntoView + 'static,
S: Clone + Send + Sync + 'static,
{
todo!()
/*match mode {
StaticMode::Incremental => {
let handler = move |req: Request<Body>| {
Box::pin({
let path = req.uri().path().to_string();
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
async move {
let (tx, rx) = futures::channel::oneshot::channel();
spawn_task!(async move {
let res = incremental_static_route(
tokio::fs::read_to_string(static_file_path(
&options, &path,
))
.await,
);
let res = handle_static_response(
path.clone(),
options,
app_fn,
additional_context,
res,
)
.await;
let _ = tx.send(res);
});
rx.await.expect("to complete HTML rendering")
}
})
};
router.route(
path,
match method {
leptos_router::Method::Get => get(handler),
leptos_router::Method::Post => post(handler),
leptos_router::Method::Put => put(handler),
leptos_router::Method::Delete => delete(handler),
leptos_router::Method::Patch => patch(handler),
},
)
}
StaticMode::Upfront => {
let handler = move |req: Request<Body>| {
Box::pin({
let path = req.uri().path().to_string();
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
async move {
let (tx, rx) = futures::channel::oneshot::channel();
spawn_task!(async move {
let res = upfront_static_route(
tokio::fs::read_to_string(static_file_path(
&options, &path,
))
.await,
);
let res = handle_static_response(
path.clone(),
options,
app_fn,
additional_context,
res,
)
.await;
let _ = tx.send(res);
});
rx.await.expect("to complete HTML rendering")
}
})
};
router.route(
path,
match method {
leptos_router::Method::Get => get(handler),
leptos_router::Method::Post => post(handler),
leptos_router::Method::Put => put(handler),
leptos_router::Method::Delete => delete(handler),
leptos_router::Method::Patch => patch(handler),
},
)
}
}*/
}
trait AxumPath {
fn to_axum_path(&self) -> String;
@@ -1611,6 +1723,7 @@ impl AxumPath for &[PathSegment] {
impl<S> LeptosRoutes<S> for axum::Router<S>
where
S: Clone + Send + Sync + 'static,
LeptosOptions: FromRef<S>,
{
#[cfg_attr(
feature = "tracing",
@@ -1688,25 +1801,24 @@ where
provide_context(method);
cx_with_state();
};
router = if let Some(static_mode) = listing.static_mode() {
router = if matches!(listing.mode(), SsrMode::Static(_)) {
#[cfg(feature = "default")]
{
static_route(
router,
router.route(
path,
app_fn.clone(),
cx_with_state_and_method.clone(),
method,
static_mode,
get(handle_static_route(
cx_with_state_and_method.clone(),
app_fn.clone(),
listing.regenerate.clone(),
)),
)
}
#[cfg(not(feature = "default"))]
{
_ = static_mode;
panic!(
"Static site generation is not currently \
supported on WASM32 server targets."
)
"Static routes are not currently supported on \
WASM32 server targets."
);
}
} else {
router.route(
@@ -1765,6 +1877,7 @@ where
leptos_router::Method::Patch => patch(s),
}
}
_ => unreachable!()
},
)
};

View File

@@ -2,9 +2,10 @@ use futures::{stream::once, Stream, StreamExt};
use hydration_context::{SharedContext, SsrSharedContext};
use leptos::{
nonce::use_nonce,
reactive_graph::owner::{Owner, Sandboxed},
reactive::owner::{Owner, Sandboxed},
IntoView,
};
use leptos_config::LeptosOptions;
use leptos_meta::ServerMetaContextOutput;
use std::{future::Future, pin::Pin, sync::Arc};
@@ -58,7 +59,7 @@ pub trait ExtendResponse: Sized {
// drop the owner, cleaning up the reactive runtime,
// once the stream is over
.chain(once(async move {
drop(owner);
owner.unset();
Default::default()
})),
));
@@ -132,3 +133,13 @@ where
}));
(owner, stream)
}
pub fn static_file_path(options: &LeptosOptions, path: &str) -> String {
let trimmed_path = path.trim_start_matches('/');
let path = if trimmed_path.is_empty() {
"index"
} else {
trimmed_path
};
format!("{}/{}.html", options.site_root, path)
}

View File

@@ -56,6 +56,7 @@ hydration = [
"reactive_graph/hydration",
"leptos_server/hydration",
"hydration_context/browser",
"leptos_dom/hydration"
]
csr = ["leptos_macro/csr", "reactive_graph/effects"]
hydrate = [

View File

@@ -3,13 +3,10 @@ use std::{
fmt::{self, Debug},
sync::Arc,
};
use tachys::{
renderer::dom::Dom,
view::{
any_view::{AnyView, IntoAny},
fragment::{Fragment, IntoFragment},
RenderHtml,
},
use tachys::view::{
any_view::{AnyView, IntoAny},
fragment::{Fragment, IntoFragment},
RenderHtml,
};
/// The most common type for the `children` property on components,
@@ -17,31 +14,31 @@ use tachys::{
///
/// This does not support iterating over individual nodes within the children.
/// To iterate over children, use [`ChildrenFragment`].
pub type Children = Box<dyn FnOnce() -> AnyView<Dom> + Send>;
pub type Children = Box<dyn FnOnce() -> AnyView + Send>;
/// A type for the `children` property on components that can be called only once,
/// and provides a collection of all the children passed to this component.
pub type ChildrenFragment = Box<dyn FnOnce() -> Fragment<Dom> + Send>;
pub type ChildrenFragment = Box<dyn FnOnce() -> Fragment + Send>;
/// A type for the `children` property on components that can be called
/// more than once.
pub type ChildrenFn = Arc<dyn Fn() -> AnyView<Dom> + Send + Sync>;
pub type ChildrenFn = Arc<dyn Fn() -> AnyView + Send + Sync>;
/// A type for the `children` property on components that can be called more than once,
/// and provides a collection of all the children passed to this component.
pub type ChildrenFragmentFn = Arc<dyn Fn() -> Fragment<Dom> + Send>;
pub type ChildrenFragmentFn = Arc<dyn Fn() -> Fragment + Send>;
/// A type for the `children` property on components that can be called
/// more than once, but may mutate the children.
pub type ChildrenFnMut = Box<dyn FnMut() -> AnyView<Dom> + Send>;
pub type ChildrenFnMut = Box<dyn FnMut() -> AnyView + Send>;
/// A type for the `children` property on components that can be called more than once,
/// but may mutate the children, and provides a collection of all the children
/// passed to this component.
pub type ChildrenFragmentMut = Box<dyn FnMut() -> Fragment<Dom> + Send>;
pub type ChildrenFragmentMut = Box<dyn FnMut() -> Fragment + Send>;
// This is to still support components that accept `Box<dyn Fn() -> AnyView>` as a children.
type BoxedChildrenFn = Box<dyn Fn() -> AnyView<Dom> + Send>;
type BoxedChildrenFn = Box<dyn Fn() -> AnyView + Send>;
/// This trait can be used when constructing a component that takes children without needing
/// to know exactly what children type the component expects. This is used internally by the
@@ -97,7 +94,7 @@ pub trait ToChildren<F> {
impl<F, C> ToChildren<F> for Children
where
F: FnOnce() -> C + Send + 'static,
C: RenderHtml<Dom> + Send + 'static,
C: RenderHtml + Send + 'static,
{
#[inline]
fn to_children(f: F) -> Self {
@@ -108,7 +105,7 @@ where
impl<F, C> ToChildren<F> for ChildrenFn
where
F: Fn() -> C + Send + Sync + 'static,
C: RenderHtml<Dom> + Send + 'static,
C: RenderHtml + Send + 'static,
{
#[inline]
fn to_children(f: F) -> Self {
@@ -119,7 +116,7 @@ where
impl<F, C> ToChildren<F> for ChildrenFnMut
where
F: Fn() -> C + Send + 'static,
C: RenderHtml<Dom> + Send + 'static,
C: RenderHtml + Send + 'static,
{
#[inline]
fn to_children(f: F) -> Self {
@@ -130,7 +127,7 @@ where
impl<F, C> ToChildren<F> for BoxedChildrenFn
where
F: Fn() -> C + Send + 'static,
C: RenderHtml<Dom> + Send + 'static,
C: RenderHtml + Send + 'static,
{
#[inline]
fn to_children(f: F) -> Self {
@@ -141,7 +138,7 @@ where
impl<F, C> ToChildren<F> for ChildrenFragment
where
F: FnOnce() -> C + Send + 'static,
C: IntoFragment<Dom>,
C: IntoFragment,
{
#[inline]
fn to_children(f: F) -> Self {
@@ -152,7 +149,7 @@ where
impl<F, C> ToChildren<F> for ChildrenFragmentFn
where
F: Fn() -> C + Send + 'static,
C: IntoFragment<Dom>,
C: IntoFragment,
{
#[inline]
fn to_children(f: F) -> Self {
@@ -163,7 +160,7 @@ where
impl<F, C> ToChildren<F> for ChildrenFragmentMut
where
F: FnMut() -> C + Send + 'static,
C: IntoFragment<Dom>,
C: IntoFragment,
{
#[inline]
fn to_children(mut f: F) -> Self {
@@ -174,7 +171,7 @@ where
/// 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)]
pub struct ViewFn(Arc<dyn Fn() -> AnyView<Dom> + Send + Sync + 'static>);
pub struct ViewFn(Arc<dyn Fn() -> AnyView + Send + Sync + 'static>);
impl Default for ViewFn {
fn default() -> Self {
@@ -185,7 +182,7 @@ impl Default for ViewFn {
impl<F, C> From<F> for ViewFn
where
F: Fn() -> C + Send + Sync + 'static,
C: RenderHtml<Dom> + Send + 'static,
C: RenderHtml + Send + 'static,
{
fn from(value: F) -> Self {
Self(Arc::new(move || value().into_any()))
@@ -194,14 +191,14 @@ where
impl ViewFn {
/// Execute the wrapped function
pub fn run(&self) -> AnyView<Dom> {
pub fn run(&self) -> AnyView {
(self.0)()
}
}
/// New-type wrapper for a function, which will only be called once and returns a view with `From` and
/// `Default` traits implemented to enable optional props in for example `<Show>` and `<Suspense>`.
pub struct ViewFnOnce(Box<dyn FnOnce() -> AnyView<Dom> + Send + 'static>);
pub struct ViewFnOnce(Box<dyn FnOnce() -> AnyView + Send + 'static>);
impl Default for ViewFnOnce {
fn default() -> Self {
@@ -212,7 +209,7 @@ impl Default for ViewFnOnce {
impl<F, C> From<F> for ViewFnOnce
where
F: FnOnce() -> C + Send + 'static,
C: RenderHtml<Dom> + Send + 'static,
C: RenderHtml + Send + 'static,
{
fn from(value: F) -> Self {
Self(Box::new(move || value().into_any()))
@@ -221,7 +218,7 @@ where
impl ViewFnOnce {
/// Execute the wrapped function
pub fn run(self) -> AnyView<Dom> {
pub fn run(self) -> AnyView {
(self.0)()
}
}

View File

@@ -4,16 +4,16 @@ use leptos_macro::component;
use reactive_graph::{
computed::ArcMemo,
effect::RenderEffect,
owner::Owner,
owner::{provide_context, Owner},
signal::ArcRwSignal,
traits::{Get, Update, With, WithUntracked},
};
use rustc_hash::FxHashMap;
use std::{fmt::Debug, marker::PhantomData, sync::Arc};
use std::{fmt::Debug, sync::Arc};
use tachys::{
html::attribute::Attribute,
hydration::Cursor,
renderer::Renderer,
reactive_graph::OwnedView,
ssr::StreamBuilder,
view::{
add_attr::AddAnyAttr, Mountable, Position, PositionState, Render,
@@ -96,27 +96,33 @@ where
let hook = hook as Arc<dyn ErrorHook>;
let _guard = throw_error::set_error_hook(Arc::clone(&hook));
let children = children.into_inner()();
ErrorBoundaryView {
hook,
boundary_id,
errors_empty,
children,
errors,
fallback,
rndr: PhantomData,
}
let owner = Owner::new();
let children = owner.with(|| {
provide_context(Arc::clone(&hook));
children.into_inner()()
});
OwnedView::new_with_owner(
ErrorBoundaryView {
hook,
boundary_id,
errors_empty,
children,
errors,
fallback,
},
owner,
)
}
struct ErrorBoundaryView<Chil, FalFn, Rndr> {
struct ErrorBoundaryView<Chil, FalFn> {
hook: Arc<dyn ErrorHook>,
boundary_id: SerializedDataId,
errors_empty: ArcMemo<bool>,
children: Chil,
fallback: FalFn,
errors: ArcRwSignal<Errors>,
rndr: PhantomData<Rndr>,
}
struct ErrorBoundaryViewState<Chil, Fal> {
@@ -125,11 +131,10 @@ struct ErrorBoundaryViewState<Chil, Fal> {
fallback: Option<Fal>,
}
impl<Chil, Fal, Rndr> Mountable<Rndr> for ErrorBoundaryViewState<Chil, Fal>
impl<Chil, Fal> Mountable for ErrorBoundaryViewState<Chil, Fal>
where
Chil: Mountable<Rndr>,
Fal: Mountable<Rndr>,
Rndr: Renderer,
Chil: Mountable,
Fal: Mountable,
{
fn unmount(&mut self) {
if let Some(fallback) = &mut self.fallback {
@@ -139,7 +144,11 @@ where
}
}
fn mount(&mut self, parent: &Rndr::Element, marker: Option<&Rndr::Node>) {
fn mount(
&mut self,
parent: &tachys::renderer::types::Element,
marker: Option<&tachys::renderer::types::Node>,
) {
if let Some(fallback) = &mut self.fallback {
fallback.mount(parent, marker);
} else {
@@ -147,7 +156,7 @@ where
}
}
fn insert_before_this(&self, child: &mut dyn Mountable<Rndr>) -> bool {
fn insert_before_this(&self, child: &mut dyn Mountable) -> bool {
if let Some(fallback) = &self.fallback {
fallback.insert_before_this(child)
} else {
@@ -156,13 +165,11 @@ where
}
}
impl<Chil, FalFn, Fal, Rndr> Render<Rndr>
for ErrorBoundaryView<Chil, FalFn, Rndr>
impl<Chil, FalFn, Fal> Render for ErrorBoundaryView<Chil, FalFn>
where
Chil: Render<Rndr> + 'static,
Chil: Render + 'static,
FalFn: FnMut(ArcRwSignal<Errors>) -> Fal + Send + 'static,
Fal: Render<Rndr> + 'static,
Rndr: Renderer,
Fal: Render + 'static,
{
type State = RenderEffect<ErrorBoundaryViewState<Chil::State, Fal::State>>;
@@ -219,26 +226,21 @@ where
}
}
impl<Chil, FalFn, Fal, Rndr> AddAnyAttr<Rndr>
for ErrorBoundaryView<Chil, FalFn, Rndr>
impl<Chil, FalFn, Fal> AddAnyAttr for ErrorBoundaryView<Chil, FalFn>
where
Chil: RenderHtml<Rndr> + 'static,
Chil: RenderHtml + 'static,
FalFn: FnMut(ArcRwSignal<Errors>) -> Fal + Send + 'static,
Fal: RenderHtml<Rndr> + Send + 'static,
Rndr: Renderer,
Fal: RenderHtml + Send + 'static,
{
type Output<SomeNewAttr: Attribute<Rndr>> = ErrorBoundaryView<
Chil::Output<SomeNewAttr::CloneableOwned>,
FalFn,
Rndr,
>;
type Output<SomeNewAttr: Attribute> =
ErrorBoundaryView<Chil::Output<SomeNewAttr::CloneableOwned>, FalFn>;
fn add_any_attr<NewAttr: Attribute<Rndr>>(
fn add_any_attr<NewAttr: Attribute>(
self,
attr: NewAttr,
) -> Self::Output<NewAttr>
where
Self::Output<NewAttr>: RenderHtml<Rndr>,
Self::Output<NewAttr>: RenderHtml,
{
let ErrorBoundaryView {
hook,
@@ -247,7 +249,6 @@ where
children,
fallback,
errors,
rndr,
} = self;
ErrorBoundaryView {
hook,
@@ -256,20 +257,17 @@ where
children: children.add_any_attr(attr.into_cloneable_owned()),
fallback,
errors,
rndr,
}
}
}
impl<Chil, FalFn, Fal, Rndr> RenderHtml<Rndr>
for ErrorBoundaryView<Chil, FalFn, Rndr>
impl<Chil, FalFn, Fal> RenderHtml for ErrorBoundaryView<Chil, FalFn>
where
Chil: RenderHtml<Rndr> + Send + 'static,
Chil: RenderHtml + Send + 'static,
FalFn: FnMut(ArcRwSignal<Errors>) -> Fal + Send + 'static,
Fal: RenderHtml<Rndr> + Send + 'static,
Rndr: Renderer,
Fal: RenderHtml + Send + 'static,
{
type AsyncOutput = ErrorBoundaryView<Chil::AsyncOutput, FalFn, Rndr>;
type AsyncOutput = ErrorBoundaryView<Chil::AsyncOutput, FalFn>;
const MIN_LENGTH: usize = Chil::MIN_LENGTH;
@@ -294,7 +292,6 @@ where
children: children.resolve().await,
fallback,
errors,
rndr: PhantomData,
}
}
@@ -368,7 +365,7 @@ where
fn hydrate<const FROM_SERVER: bool>(
mut self,
cursor: &Cursor<Rndr>,
cursor: &Cursor,
position: &PositionState,
) -> Self::State {
let mut children = Some(self.children);

View File

@@ -157,7 +157,7 @@ where
};
move || keyed(each(), key.clone(), children.clone())
}
/*
#[cfg(test)]
mod tests {
use crate::prelude::*;
@@ -168,7 +168,7 @@ mod tests {
fn creates_list() {
Owner::new().with(|| {
let values = RwSignal::new(vec![1, 2, 3, 4, 5]);
let list: View<HtmlElement<_, _, _, Dom>> = view! {
let list: View<HtmlElement<_, _, _>> = view! {
<ol>
<For each=move || values.get() key=|i| *i let:i>
<li>{i}</li>
@@ -187,7 +187,7 @@ mod tests {
fn creates_list_enumerate() {
Owner::new().with(|| {
let values = RwSignal::new(vec![1, 2, 3, 4, 5]);
let list: View<HtmlElement<_, _, _, Dom>> = view! {
let list: View<HtmlElement<_, _, _>> = view! {
<ol>
<ForEnumerate each=move || values.get() key=|i| *i let(index, i)>
<li>{move || index.get()}"-"{i}</li>
@@ -200,7 +200,7 @@ mod tests {
<!>-<!>4</li><li>4<!>-<!>5</li><!></ol>"
);
let list: View<HtmlElement<_, _, _, Dom>> = view! {
let list: View<HtmlElement<_, _, _>> = view! {
<ol>
<ForEnumerate each=move || values.get() key=|i| *i let(index, i)>
<li>{move || index.get()}"-"{i}</li>
@@ -216,3 +216,4 @@ mod tests {
});
}
}
*/

View File

@@ -1,8 +1,8 @@
(function (pkg_path, output_name, wasm_output_name) {
import(`/${pkg_path}/${output_name}.js`)
(function (root, pkg_path, output_name, wasm_output_name) {
import(`${root}/${pkg_path}/${output_name}.js`)
.then(mod => {
mod.default(`/${pkg_path}/${wasm_output_name}.wasm`).then(() => {
mod.default(`${root}/${pkg_path}/${wasm_output_name}.wasm`).then(() => {
mod.hydrate();
});
})
})
})

View File

@@ -1,4 +1,4 @@
((pkg_path, output_name, wasm_output_name) => {
((root, pkg_path, output_name, wasm_output_name) => {
function idle(c) {
if ("requestIdleCallback" in window) {
window.requestIdleCallback(c);
@@ -34,11 +34,11 @@
return { el: null, id: null, children: tree };
}
function hydrateIsland(el, id, mod) {
const islandFn = mod[`_island_${id}`];
const islandFn = mod[id];
if (islandFn) {
islandFn(el);
} else {
console.warn(`Could not find WASM function for the island ${l}.`);
console.warn(`Could not find WASM function for the island ${id}.`);
}
}
function hydrateIslands(entry, mod) {
@@ -50,9 +50,9 @@
}
}
idle(() => {
import(`/${pkg_path}/${output_name}.js`)
import(`${root}/${pkg_path}/${output_name}.js`)
.then(mod => {
mod.default(`/${pkg_path}/${wasm_output_name}.wasm`).then(() => {
mod.default(`${root}/${pkg_path}/${wasm_output_name}.wasm`).then(() => {
mod.hydrate();
hydrateIslands(islandTree(document.body, null), mod);
});

View File

@@ -38,13 +38,41 @@ pub fn AutoReload(
pub fn HydrationScripts(
options: LeptosOptions,
#[prop(optional)] islands: bool,
/// A base url, not including a trailing slash
#[prop(optional, into)]
root: Option<String>,
) -> impl IntoView {
let pkg_path = &options.site_pkg_dir;
let output_name = &options.output_name;
let mut wasm_output_name = output_name.clone();
if std::option_env!("LEPTOS_OUTPUT_NAME").is_none() {
wasm_output_name.push_str("_bg");
let mut js_file_name = options.output_name.to_string();
let mut wasm_file_name = options.output_name.to_string();
if options.hash_files {
let hash_path = std::env::current_exe()
.map(|path| {
path.parent().map(|p| p.to_path_buf()).unwrap_or_default()
})
.unwrap_or_default()
.join(&options.hash_file);
if hash_path.exists() {
let hashes = std::fs::read_to_string(&hash_path)
.expect("failed to read hash file");
for line in hashes.lines() {
let line = line.trim();
if !line.is_empty() {
if let Some((file, hash)) = line.split_once(':') {
if file == "js" {
js_file_name.push_str(&format!(".{}", hash.trim()));
} else if file == "wasm" {
wasm_file_name
.push_str(&format!(".{}", hash.trim()));
}
}
}
}
}
} else if std::option_env!("LEPTOS_OUTPUT_NAME").is_none() {
wasm_file_name.push_str("_bg");
}
let pkg_path = &options.site_pkg_dir;
#[cfg(feature = "nonce")]
let nonce = crate::nonce::use_nonce();
#[cfg(not(feature = "nonce"))]
@@ -58,17 +86,18 @@ pub fn HydrationScripts(
include_str!("./hydration_script.js")
};
let root = root.unwrap_or_default();
view! {
<link rel="modulepreload" href=format!("/{pkg_path}/{output_name}.js") nonce=nonce.clone()/>
<link rel="modulepreload" href=format!("{root}/{pkg_path}/{js_file_name}.js") nonce=nonce.clone()/>
<link
rel="preload"
href=format!("/{pkg_path}/{wasm_output_name}.wasm")
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}({pkg_path:?}, {output_name:?}, {wasm_output_name:?})")}
{format!("{script}({root:?}, {pkg_path:?}, {js_file_name:?}, {wasm_file_name:?})")}
</script>
}
}

View File

@@ -2,7 +2,6 @@ use std::borrow::Cow;
use tachys::{
html::attribute::Attribute,
hydration::Cursor,
renderer::{dom::Dom, Renderer},
ssr::StreamBuilder,
view::{
add_attr::AddAnyAttr, Position, PositionState, Render, RenderHtml,
@@ -50,14 +49,14 @@ impl<T> View<T> {
pub trait IntoView
where
Self: Sized + Render<Dom> + RenderHtml<Dom> + Send,
Self: Sized + Render + RenderHtml + Send,
{
fn into_view(self) -> View<Self>;
}
impl<T> IntoView for T
where
T: Sized + Render<Dom> + RenderHtml<Dom> + Send, //+ AddAnyAttr<Dom>,
T: Sized + Render + RenderHtml + Send, //+ AddAnyAttr,
{
fn into_view(self) -> View<Self> {
View {
@@ -68,7 +67,7 @@ where
}
}
impl<T: Render<Rndr>, Rndr: Renderer> Render<Rndr> for View<T> {
impl<T: Render> Render for View<T> {
type State = T::State;
fn build(self) -> Self::State {
@@ -80,10 +79,10 @@ impl<T: Render<Rndr>, Rndr: Renderer> Render<Rndr> for View<T> {
}
}
impl<T: RenderHtml<Rndr>, Rndr: Renderer> RenderHtml<Rndr> for View<T> {
impl<T: RenderHtml> RenderHtml for View<T> {
type AsyncOutput = T::AsyncOutput;
const MIN_LENGTH: usize = <T as RenderHtml<Rndr>>::MIN_LENGTH;
const MIN_LENGTH: usize = <T as RenderHtml>::MIN_LENGTH;
async fn resolve(self) -> Self::AsyncOutput {
self.inner.resolve().await
@@ -147,7 +146,7 @@ impl<T: RenderHtml<Rndr>, Rndr: Renderer> RenderHtml<Rndr> for View<T> {
fn hydrate<const FROM_SERVER: bool>(
self,
cursor: &Cursor<Rndr>,
cursor: &Cursor,
position: &PositionState,
) -> Self::State {
self.inner.hydrate::<FROM_SERVER>(cursor, position)
@@ -166,18 +165,15 @@ impl<T: ToTemplate> ToTemplate for View<T> {
}
}
impl<T: AddAnyAttr<Rndr>, Rndr> AddAnyAttr<Rndr> for View<T>
where
Rndr: Renderer,
{
type Output<SomeNewAttr: Attribute<Rndr>> = View<T::Output<SomeNewAttr>>;
impl<T: AddAnyAttr> AddAnyAttr for View<T> {
type Output<SomeNewAttr: Attribute> = View<T::Output<SomeNewAttr>>;
fn add_any_attr<NewAttr: Attribute<Rndr>>(
fn add_any_attr<NewAttr: Attribute>(
self,
attr: NewAttr,
) -> Self::Output<NewAttr>
where
Self::Output<NewAttr>: RenderHtml<Rndr>,
Self::Output<NewAttr>: RenderHtml,
{
let View {
inner,

View File

@@ -4,13 +4,13 @@
//!
//! Leptos is a full-stack framework for building web applications in Rust. You can use it to build
//! - single-page apps (SPAs) rendered entirely in the browser, using client-side routing and loading
//! or mutating data via async requests to the server
//! or mutating data via async requests to the server.
//! - multi-page apps (MPAs) rendered on the server, managing navigation, data, and mutations via
//! web-standard `<a>` and `<form>` tags
//! web-standard `<a>` and `<form>` tags.
//! - progressively-enhanced single-page apps that are rendered on the server and then hydrated on the client,
//! enhancing your `<a>` and `<form>` navigations and mutations seamlessly when WASM is available.
//!
//! And you can do all three of these **using the same Leptos code.**
//! And you can do all three of these **using the same Leptos code**.
//!
//! Take a look at the [Leptos Book](https://leptos-rs.github.io/leptos/) for a walkthrough of the framework.
//! Join us on our [Discord Channel](https://discord.gg/v38Eef6sWG) to see what the community is building.
@@ -20,76 +20,76 @@
//!
//! If you want to see what Leptos is capable of, check out
//! the [examples](https://github.com/leptos-rs/leptos/tree/main/examples):
//! - [`counter`](https://github.com/leptos-rs/leptos/tree/main/examples/counter) is the classic
//! counter example, showing the basics of client-side rendering and reactive DOM updates
//! - [`counter_without_macros`](https://github.com/leptos-rs/leptos/tree/main/examples/counter_without_macros)
//! adapts the counter example to use the builder pattern for the UI and avoids other macros, instead showing
//! the code that Leptos generates.
//! - [`counters`](https://github.com/leptos-rs/leptos/tree/main/examples/counters) introduces parent-child
//! communication via contexts, and the `<For/>` component for efficient keyed list updates.
//! - [`error_boundary`](https://github.com/leptos-rs/leptos/tree/main/examples/error_boundary) shows how to use
//! `Result` types to handle errors.
//! - [`parent_child`](https://github.com/leptos-rs/leptos/tree/main/examples/parent_child) shows four different
//! ways a parent component can communicate with a child, including passing a closure, context, and more
//! - [`fetch`](https://github.com/leptos-rs/leptos/tree/main/examples/fetch) introduces
//! [Resource]s, which allow you to integrate arbitrary `async` code like an
//!
//! - **[`counter`]** is the classic counter example, showing the basics of client-side rendering and reactive DOM updates.
//! - **[`counter_without_macros`]** adapts the counter example to use the builder pattern for the UI and avoids other macros,
//! instead showing the code that Leptos generates.
//! - **[`counters`]** introduces parent-child communication via contexts, and the [`<For/>`](leptos::prelude::For) component
//! for efficient keyed list updates.
//! - **[`error_boundary`]** shows how to use [`Result`] types to handle errors.
//! - **[`parent_child`]** shows four different ways a parent component can communicate with a child, including passing a closure,
//! context, and more.
//! - **[`fetch`]** introduces [`Resource`](leptos::prelude::Resource)s, which allow you to integrate arbitrary `async` code like an
//! HTTP request within your reactive code.
//! - [`router`](https://github.com/leptos-rs/leptos/tree/main/examples/router) shows how to use Leptoss nested router
//! to enable client-side navigation and route-specific, reactive data loading.
//! - [`slots`](https://github.com/leptos-rs/leptos/tree/main/examples/slots) shows how to use slots on components.
//! - [`spread`](https://github.com/leptos-rs/leptos/tree/main/examples/spread) shows how the spread syntax can be used to spread data and/or event handlers onto elements.
//! - [`counter_isomorphic`](https://github.com/leptos-rs/leptos/tree/main/examples/counter_isomorphic) shows
//! different methods of interaction with a stateful server, including server functions, server actions, forms,
//! and server-sent events (SSE).
//! - [`todomvc`](https://github.com/leptos-rs/leptos/tree/main/examples/todomvc) shows the basics of building an
//! isomorphic web app. Both the server and the client import the same app code from the `todomvc` example.
//! - **[`router`]** shows how to use Leptoss nested router to enable client-side navigation and route-specific, reactive data loading.
//! - **[`slots`]** shows how to use slots on components.
//! - **[`spread`]** shows how the spread syntax can be used to spread data and/or event handlers onto elements.
//! - **[`counter_isomorphic`]** shows different methods of interaction with a stateful server, including server functions,
//! server actions, forms, and server-sent events (SSE).
//! - **[`todomvc`]** shows the basics of building an isomorphic web app. Both the server and the client import the same app code.
//! The server renders the app directly to an HTML string, and the client hydrates that HTML to make it interactive.
//! You might also want to
//! see how we use [`create_effect`] to [serialize JSON to `localStorage`](https://github.com/leptos-rs/leptos/blob/16f084a71268ac325fbc4a5e50c260df185eadb6/examples/todomvc/src/lib.rs#L164)
//! and [reactively call DOM methods](https://github.com/leptos-rs/leptos/blob/6d7c36655c9e7dcc3a3ad33d2b846a3f00e4ae74/examples/todomvc/src/lib.rs#L291)
//! on [references to elements](https://github.com/leptos-rs/leptos/blob/6d7c36655c9e7dcc3a3ad33d2b846a3f00e4ae74/examples/todomvc/src/lib.rs#L254).
//! - [`hackernews`](https://github.com/leptos-rs/leptos/tree/main/examples/hackernews)
//! and [`hackernews_axum`](https://github.com/leptos-rs/leptos/tree/main/examples/hackernews_axum)
//! integrate calls to a real external REST API, routing, server-side rendering and hydration to create
//! a fully-functional application that works as intended even before WASM has loaded and begun to run.
//! - [`todo_app_sqlite`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite) and
//! [`todo_app_sqlite_axum`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite_axum)
//! show how to build a full-stack app using server functions and database connections.
//! - [`tailwind`](https://github.com/leptos-rs/leptos/tree/main/examples/tailwind_csr) shows how to integrate
//! TailwindCSS with `trunk` for CSR.
//! You might also want to see how we use [`Effect::new`](leptos::prelude::Effect::new) to
//! [serialize JSON to `localStorage`](https://github.com/leptos-rs/leptos/blob/20af4928b2fffe017408d3f4e7330db22cf68277/examples/todomvc/src/lib.rs#L191-L209)
//! and [reactively call DOM methods](https://github.com/leptos-rs/leptos/blob/16f084a71268ac325fbc4a5e50c260df185eadb6/examples/todomvc/src/lib.rs#L292-L296)
//! on [references to elements](https://github.com/leptos-rs/leptos/blob/20af4928b2fffe017408d3f4e7330db22cf68277/examples/todomvc/src/lib.rs#L228).
//! - **[`hackernews`]** and **[`hackernews_axum`]** integrate calls to a real external REST API, routing, server-side rendering and
//! hydration to create a fully-functional application that works as intended even before WASM has loaded and begun to run.
//! - **[`todo_app_sqlite`]** and **[`todo_app_sqlite_axum`]** show how to build a full-stack app using server functions and
//! database connections.
//! - **[`tailwind`]** shows how to integrate TailwindCSS with `trunk` for CSR.
//!
//! [`counter`]: https://github.com/leptos-rs/leptos/tree/main/examples/counter
//! [`counter_without_macros`]: https://github.com/leptos-rs/leptos/tree/main/examples/counter_without_macros
//! [`counters`]: https://github.com/leptos-rs/leptos/tree/main/examples/counters
//! [`error_boundary`]: https://github.com/leptos-rs/leptos/tree/main/examples/error_boundary
//! [`parent_child`]: https://github.com/leptos-rs/leptos/tree/main/examples/parent_child
//! [`fetch`]: https://github.com/leptos-rs/leptos/tree/main/examples/fetch
//! [`router`]: https://github.com/leptos-rs/leptos/tree/main/examples/router
//! [`slots`]: https://github.com/leptos-rs/leptos/tree/main/examples/slots
//! [`spread`]: https://github.com/leptos-rs/leptos/tree/main/examples/spread
//! [`counter_isomorphic`]: https://github.com/leptos-rs/leptos/tree/main/examples/counter_isomorphic
//! [`todomvc`]: https://github.com/leptos-rs/leptos/tree/main/examples/todomvc
//! [`hackernews`]: https://github.com/leptos-rs/leptos/tree/main/examples/hackernews
//! [`hackernews_axum`]: https://github.com/leptos-rs/leptos/tree/main/examples/hackernews_axum
//! [`todo_app_sqlite`]: https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite
//! [`todo_app_sqlite_axum`]: https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite_axum
//! [`tailwind`]: https://github.com/leptos-rs/leptos/tree/main/examples/tailwind_csr
//!
//! Details on how to run each example can be found in its README.
//!
//! # Quick Links
//!
//! Here are links to the most important sections of the docs:
//! - **Reactivity**: the [`leptos_reactive`] overview, and more details in
//! - signals: [`create_signal`], [`ReadSignal`], and [`WriteSignal`] (and [`create_rw_signal`] and [`RwSignal`])
//! - computations: [`create_memo`] and [`Memo`]
//! - `async` interop: [`create_resource`] and [`Resource`] for loading data using `async` functions,
//! and [`create_action`] and [`Action`] to mutate data or imperatively call `async` functions.
//! - reactions: [`create_effect`]
//! - **Templating/Views**: the [`view`] macro
//! - **Reactivity**: the [`reactive_graph`] overview, and more details in
//! + signals: [`signal`](leptos::prelude::signal), [`ReadSignal`](leptos::prelude::ReadSignal),
//! [`WriteSignal`](leptos::prelude::WriteSignal) and [`RwSignal`](leptos::prelude::RwSignal).
//! + computations: [`Memo`](leptos::prelude::Memo).
//! + `async` interop: [`Resource`](leptos::prelude::Resource) for loading data using `async` functions
//! and [`Action`](leptos::prelude::Action) to mutate data or imperatively call `async` functions.
//! + reactions: [`Effect`](leptos::prelude::Effect) and [`RenderEffect`](leptos::prelude::RenderEffect).
//! - **Templating/Views**: the [`view`] macro and [`IntoView`](leptos::IntoView) trait.
//! - **Routing**: the [`leptos_router`](https://docs.rs/leptos_router/latest/leptos_router/) crate
//! - **Server Functions**: the [`server`](crate::leptos_server) macro, [`create_action`], and [`create_server_action`]
//! - **Server Functions**: the [`server`](macro@leptos::prelude::server) macro and [`ServerAction`](leptos::prelude::ServerAction).
//!
//! # Feature Flags
//! - `nightly`: On `nightly` Rust, enables the function-call syntax for signal getters and setters.
//! - `csr` Client-side rendering: Generate DOM nodes in the browser
//! - `ssr` Server-side rendering: Generate an HTML string (typically on the server)
//! - `hydrate` Hydration: use this to add interactivity to an SSRed Leptos app
//! - `serde` (*Default*) In SSR/hydrate mode, uses [`serde`](https://docs.rs/serde/latest/serde/) to serialize resources and send them
//!
//! - **`nightly`**: On `nightly` Rust, enables the function-call syntax for signal getters and setters.
//! - **`csr`** Client-side rendering: Generate DOM nodes in the browser.
//! - **`ssr`** Server-side rendering: Generate an HTML string (typically on the server).
//! - **`hydrate`** Hydration: use this to add interactivity to an SSRed Leptos app.
//! - **`rkyv`** In SSR/hydrate mode, uses [`rkyv`](https://docs.rs/rkyv/latest/rkyv/) to serialize resources and send them
//! from the server to the client.
//! - `serde-lite` In SSR/hydrate mode, uses [`serde-lite`](https://docs.rs/serde-lite/latest/serde_lite/) to serialize resources and send them
//! from the server to the client.
//! - `rkyv` In SSR/hydrate mode, uses [`rkyv`](https://docs.rs/rkyv/latest/rkyv/) to serialize resources and send them
//! from the server to the client.
//! - `miniserde` In SSR/hydrate mode, uses [`miniserde`](https://docs.rs/miniserde/latest/miniserde/) to serialize resources and send them
//! from the server to the client.
//! - `tracing` Adds additional support for [`tracing`](https://docs.rs/tracing/latest/tracing/) to components.
//! - `default-tls` Use default native TLS support. (Only applies when using server functions with a non-WASM client like a desktop app.)
//! - `rustls` Use `rustls`. (Only applies when using server functions with a non-WASM client like a desktop app.)
//! - `template_macro` Enables the [`template!`](leptos_macro::template) macro, which offers faster DOM node creation for some use cases in `csr`.
//! - **`tracing`** Adds support for [`tracing`](https://docs.rs/tracing/latest/tracing/).
//!
//! **Important Note:** You must enable one of `csr`, `hydrate`, or `ssr` to tell Leptos
//! which mode your app is operating in. You should only enable one of these per build target,
@@ -103,7 +103,7 @@
//! #[component]
//! pub fn SimpleCounter(initial_value: i32) -> impl IntoView {
//! // create a reactive signal with the initial value
//! let (value, set_value) = 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
@@ -112,7 +112,6 @@
//! let increment = move |_| *set_value.write() += 1;
//!
//! view! {
//!
//! <div>
//! <button on:click=clear>"Clear"</button>
//! <button on:click=decrement>"-1"</button>
@@ -124,7 +123,8 @@
//! ```
//!
//! Leptos is easy to use with [Trunk](https://trunkrs.dev/) (or with a simple wasm-bindgen setup):
//! ```
//!
//! ```rust
//! use leptos::{mount::mount_to_body, prelude::*};
//!
//! #[component]
@@ -168,13 +168,12 @@ pub mod prelude {
pub use leptos_server::*;
pub use oco_ref::*;
pub use reactive_graph::{
actions::*, computed::*, effect::*, owner::*, signal::*,
wrappers::read::*, *,
actions::*, computed::*, effect::*, graph::untrack, owner::*,
signal::*, wrappers::read::*,
};
pub use server_fn::{self, ServerFnError};
pub use tachys::{
self,
reactive_graph::{node_ref::*, Suspend},
reactive_graph::{bind::BindAttribute, node_ref::*, Suspend},
view::template::ViewTemplate,
};
}
@@ -230,6 +229,7 @@ mod suspense_component;
pub mod text_prop;
mod transition;
pub use leptos_macro::*;
#[doc(inline)]
pub use server_fn;
#[doc(hidden)]
pub use typed_builder;
@@ -237,16 +237,22 @@ pub use typed_builder;
pub use typed_builder_macro;
mod into_view;
pub use into_view::IntoView;
#[doc(inline)]
pub use leptos_dom;
mod provider;
#[doc(inline)]
pub use tachys;
/// Tools to mount an application to the DOM, or to hydrate it from server-rendered HTML.
pub mod mount;
#[doc(inline)]
pub use leptos_config as config;
#[doc(inline)]
pub use oco_ref as oco;
mod from_form_data;
#[doc(inline)]
pub use either_of as either;
pub use reactive_graph;
#[doc(inline)]
pub use reactive_graph as reactive;
/// Provide and access data along the reactive graph, sharing data without directly passing arguments.
pub mod context {
@@ -254,17 +260,22 @@ pub mod context {
pub use reactive_graph::owner::{provide_context, use_context};
}
#[doc(inline)]
pub use leptos_server as server;
/// HTML attribute types.
#[doc(inline)]
pub use tachys::html::attribute as attr;
/// HTML element types.
#[doc(inline)]
pub use tachys::html::element as html;
/// HTML event types.
#[doc(no_inline)]
pub use tachys::html::event as ev;
/// MathML element types.
#[doc(inline)]
pub use tachys::mathml as math;
/// SVG element types.
#[doc(inline)]
pub use tachys::svg;
/// Utilities for simple isomorphic logging to the console or terminal.
@@ -272,7 +283,7 @@ pub mod logging {
pub use leptos_dom::{debug_warn, error, log, warn};
}
pub mod spawn {
pub mod task {
pub use any_spawner::Executor;
use std::future::Future;
@@ -290,9 +301,14 @@ pub mod spawn {
Executor::spawn_local(fut)
}
/// Waits until the next "tick" of the current async executor.
pub async fn tick() {
Executor::tick().await
}
pub use reactive_graph::{
spawn_local_scoped, spawn_local_scoped_with_cancellation,
};
}
// these reexports are used in islands

View File

@@ -5,10 +5,8 @@ use any_spawner::Executor;
use reactive_graph::owner::Owner;
#[cfg(debug_assertions)]
use std::cell::Cell;
use std::marker::PhantomData;
use tachys::{
dom::body,
renderer::{dom::Dom, Renderer},
view::{Mountable, Render},
};
#[cfg(feature = "hydrate")]
@@ -38,10 +36,7 @@ thread_local! {
#[cfg(feature = "hydrate")]
/// Runs the provided closure and mounts the result to the provided element.
pub fn hydrate_from<F, N>(
parent: HtmlElement,
f: F,
) -> UnmountHandle<N::State, Dom>
pub fn hydrate_from<F, N>(parent: HtmlElement, f: F) -> UnmountHandle<N::State>
where
F: FnOnce() -> N + 'static,
N: IntoView,
@@ -85,11 +80,7 @@ where
// returns a handle that owns the owner
// when this is dropped, it will clean up the reactive system and unmount the view
UnmountHandle {
owner,
mountable,
rndr: PhantomData,
}
UnmountHandle { owner, mountable }
}
/// Runs the provided closure and mounts the result to the `<body>`.
@@ -103,7 +94,7 @@ where
}
/// Runs the provided closure and mounts the result to the provided element.
pub fn mount_to<F, N>(parent: HtmlElement, f: F) -> UnmountHandle<N::State, Dom>
pub fn mount_to<F, N>(parent: HtmlElement, f: F) -> UnmountHandle<N::State>
where
F: FnOnce() -> N + 'static,
N: IntoView,
@@ -140,22 +131,17 @@ where
// returns a handle that owns the owner
// when this is dropped, it will clean up the reactive system and unmount the view
UnmountHandle {
owner,
mountable,
rndr: PhantomData,
}
UnmountHandle { owner, mountable }
}
/// Runs the provided closure and mounts the result to the provided element.
pub fn mount_to_renderer<F, N, R>(
parent: &R::Element,
pub fn mount_to_renderer<F, N>(
parent: &tachys::renderer::types::Element,
f: F,
) -> UnmountHandle<N::State, R>
) -> UnmountHandle<N::State>
where
F: FnOnce() -> N + 'static,
N: Render<R>,
R: Renderer,
N: Render,
{
// use wasm-bindgen-futures to drive the reactive system
// we ignore the return value because an Err here just means the wasm-bindgen executor is
@@ -173,11 +159,7 @@ where
// returns a handle that owns the owner
// when this is dropped, it will clean up the reactive system and unmount the view
UnmountHandle {
owner,
mountable,
rndr: PhantomData,
}
UnmountHandle { owner, mountable }
}
/// Hydrates any islands that are currently present on the page.
@@ -211,21 +193,18 @@ pub fn hydrate_islands() {
reactive system. You should either call `.forget()` to keep the \
view permanently mounted, or store the `UnmountHandle` somewhere \
and drop it when you'd like to unmount the view."]
pub struct UnmountHandle<M, R>
pub struct UnmountHandle<M>
where
M: Mountable<R>,
R: Renderer,
M: Mountable,
{
#[allow(dead_code)]
owner: Owner,
mountable: M,
rndr: PhantomData<R>,
}
impl<M, R> UnmountHandle<M, R>
impl<M> UnmountHandle<M>
where
M: Mountable<R>,
R: Renderer,
M: Mountable,
{
/// Leaks the handle, preventing the reactive system from being cleaned up and the view from
/// being unmounted. This should always be called when [`mount_to`] is used for the root of an
@@ -235,10 +214,9 @@ where
}
}
impl<M, R> Drop for UnmountHandle<M, R>
impl<M> Drop for UnmountHandle<M>
where
M: Mountable<R>,
R: Renderer,
M: Mountable,
{
fn drop(&mut self) {
self.mountable.unmount();

View File

@@ -6,7 +6,7 @@ use base64::{
};
use rand::{thread_rng, RngCore};
use std::{fmt::Display, ops::Deref, sync::Arc};
use tachys::{html::attribute::AttributeValue, renderer::Renderer};
use tachys::html::attribute::AttributeValue;
/// A cryptographic nonce ("number used once") which can be
/// used by Content Security Policy to determine whether or not a given
@@ -65,12 +65,9 @@ impl Display for Nonce {
}
}
impl<R> AttributeValue<R> for Nonce
where
R: Renderer,
{
impl AttributeValue for Nonce {
type AsyncOutput = Self;
type State = <Arc<str> as AttributeValue<R>>::State;
type State = <Arc<str> as AttributeValue>::State;
type Cloneable = Self;
type CloneableOwned = Self;
@@ -79,7 +76,7 @@ where
}
fn to_html(self, key: &str, buf: &mut String) {
<Arc<str> as AttributeValue<R>>::to_html(self.0, key, buf)
<Arc<str> as AttributeValue>::to_html(self.0, key, buf)
}
fn to_template(_key: &str, _buf: &mut String) {}
@@ -87,17 +84,21 @@ where
fn hydrate<const FROM_SERVER: bool>(
self,
key: &str,
el: &<R as Renderer>::Element,
el: &tachys::renderer::types::Element,
) -> Self::State {
<Arc<str> as AttributeValue<R>>::hydrate::<FROM_SERVER>(self.0, key, el)
<Arc<str> as AttributeValue>::hydrate::<FROM_SERVER>(self.0, key, el)
}
fn build(self, el: &<R as Renderer>::Element, key: &str) -> Self::State {
<Arc<str> as AttributeValue<R>>::build(self.0, el, key)
fn build(
self,
el: &tachys::renderer::types::Element,
key: &str,
) -> Self::State {
<Arc<str> as AttributeValue>::build(self.0, el, key)
}
fn rebuild(self, key: &str, state: &mut Self::State) {
<Arc<str> as AttributeValue<R>>::rebuild(self.0, key, state)
<Arc<str> as AttributeValue>::rebuild(self.0, key, state)
}
fn into_cloneable(self) -> Self::Cloneable {

View File

@@ -1,7 +1,7 @@
use crate::{children::TypedChildrenFn, mount, IntoView};
use leptos_dom::helpers::document;
use leptos_macro::component;
use reactive_graph::{effect::Effect, owner::Owner, untrack};
use reactive_graph::{effect::Effect, graph::untrack, owner::Owner};
use std::sync::Arc;
/// Renders components somewhere else in the DOM.

View File

@@ -13,7 +13,7 @@ use reactive_graph::{
effect::RenderEffect,
owner::{provide_context, use_context, Owner},
signal::ArcRwSignal,
traits::{Get, Read, Track, With},
traits::{Dispose, Get, Read, Track, With},
};
use slotmap::{DefaultKey, SlotMap};
use tachys::{
@@ -21,7 +21,6 @@ use tachys::{
html::attribute::Attribute,
hydration::Cursor,
reactive_graph::{OwnedView, OwnedViewState},
renderer::Renderer,
ssr::StreamBuilder,
view::{
add_attr::AddAnyAttr,
@@ -135,15 +134,14 @@ pub(crate) struct SuspenseBoundary<const TRANSITION: bool, Fal, Chil> {
pub children: Chil,
}
impl<const TRANSITION: bool, Fal, Chil, Rndr> Render<Rndr>
impl<const TRANSITION: bool, Fal, Chil> Render
for SuspenseBoundary<TRANSITION, Fal, Chil>
where
Fal: Render<Rndr> + Send + 'static,
Chil: Render<Rndr> + Send + 'static,
Rndr: Renderer + 'static,
Fal: Render + Send + 'static,
Chil: Render + Send + 'static,
{
type State = RenderEffect<
OwnedViewState<EitherKeepAliveState<Chil::State, Fal::State>, Rndr>,
OwnedViewState<EitherKeepAliveState<Chil::State, Fal::State>>,
>;
fn build(self) -> Self::State {
@@ -187,25 +185,24 @@ where
}
}
impl<const TRANSITION: bool, Fal, Chil, Rndr> AddAnyAttr<Rndr>
impl<const TRANSITION: bool, Fal, Chil> AddAnyAttr
for SuspenseBoundary<TRANSITION, Fal, Chil>
where
Fal: RenderHtml<Rndr> + Send + 'static,
Chil: RenderHtml<Rndr> + Send + 'static,
Rndr: Renderer + 'static,
Fal: RenderHtml + Send + 'static,
Chil: RenderHtml + Send + 'static,
{
type Output<SomeNewAttr: Attribute<Rndr>> = SuspenseBoundary<
type Output<SomeNewAttr: Attribute> = SuspenseBoundary<
TRANSITION,
Fal,
Chil::Output<SomeNewAttr::CloneableOwned>,
>;
fn add_any_attr<NewAttr: Attribute<Rndr>>(
fn add_any_attr<NewAttr: Attribute>(
self,
attr: NewAttr,
) -> Self::Output<NewAttr>
where
Self::Output<NewAttr>: RenderHtml<Rndr>,
Self::Output<NewAttr>: RenderHtml,
{
let attr = attr.into_cloneable_owned();
let SuspenseBoundary {
@@ -223,12 +220,11 @@ where
}
}
impl<const TRANSITION: bool, Fal, Chil, Rndr> RenderHtml<Rndr>
impl<const TRANSITION: bool, Fal, Chil> RenderHtml
for SuspenseBoundary<TRANSITION, Fal, Chil>
where
Fal: RenderHtml<Rndr> + Send + 'static,
Chil: RenderHtml<Rndr> + Send + 'static,
Rndr: Renderer + 'static,
Fal: RenderHtml + Send + 'static,
Chil: RenderHtml + Send + 'static,
{
// i.e., if this is the child of another Suspense during SSR, don't wait for it: it will handle
// itself
@@ -264,7 +260,6 @@ where
{
buf.next_id();
let suspense_context = use_context::<SuspenseContext>().unwrap();
let owner = Owner::current().unwrap();
// we need to wait for one of two things: either
@@ -277,7 +272,17 @@ where
futures::channel::oneshot::channel::<()>();
let mut tasks_tx = Some(tasks_tx);
let eff = reactive_graph::effect::RenderEffect::new_isomorphic({
// now, create listener for local resources
let (local_tx, mut local_rx) =
futures::channel::oneshot::channel::<()>();
provide_context(LocalResourceNotifier::from(local_tx));
// walk over the tree of children once to make sure that all resource loads are registered
self.children.dry_resolve();
// check the set of tasks to see if it is empty, now or later
let eff = reactive_graph::effect::Effect::new_isomorphic({
move |_| {
tasks.track();
if tasks.read().is_empty() {
@@ -290,14 +295,6 @@ where
}
});
// now, create listener for local resources
let (local_tx, mut local_rx) =
futures::channel::oneshot::channel::<()>();
provide_context(LocalResourceNotifier::from(local_tx));
// walk over the tree of children once to make sure that all resource loads are registered
self.children.dry_resolve();
let mut fut = Box::pin(ScopedFuture::new(ErrorHookFuture::new(
async move {
// race the local resource notifier against the set of tasks
@@ -337,7 +334,7 @@ where
}
children = children => {
// clean up the (now useless) effect
drop(eff);
eff.dispose();
Some(OwnedView::new_with_owner(children, owner))
}
@@ -404,7 +401,7 @@ where
fn hydrate<const FROM_SERVER: bool>(
self,
cursor: &Cursor<Rndr>,
cursor: &Cursor,
position: &PositionState,
) -> Self::State {
let cursor = cursor.to_owned();
@@ -454,10 +451,9 @@ impl<T> Unsuspend<T> {
}
}
impl<T, Rndr> Render<Rndr> for Unsuspend<T>
impl<T> Render for Unsuspend<T>
where
T: Render<Rndr>,
Rndr: Renderer,
T: Render,
{
type State = T::State;
@@ -470,30 +466,28 @@ where
}
}
impl<T, Rndr> AddAnyAttr<Rndr> for Unsuspend<T>
impl<T> AddAnyAttr for Unsuspend<T>
where
T: AddAnyAttr<Rndr> + 'static,
Rndr: Renderer,
T: AddAnyAttr + 'static,
{
type Output<SomeNewAttr: Attribute<Rndr>> =
type Output<SomeNewAttr: Attribute> =
Unsuspend<T::Output<SomeNewAttr::CloneableOwned>>;
fn add_any_attr<NewAttr: Attribute<Rndr>>(
fn add_any_attr<NewAttr: Attribute>(
self,
attr: NewAttr,
) -> Self::Output<NewAttr>
where
Self::Output<NewAttr>: RenderHtml<Rndr>,
Self::Output<NewAttr>: RenderHtml,
{
let attr = attr.into_cloneable_owned();
Unsuspend::new(move || (self.0)().add_any_attr(attr))
}
}
impl<T, Rndr> RenderHtml<Rndr> for Unsuspend<T>
impl<T> RenderHtml for Unsuspend<T>
where
T: RenderHtml<Rndr> + 'static,
Rndr: Renderer,
T: RenderHtml + 'static,
{
type AsyncOutput = Self;
@@ -534,7 +528,7 @@ where
fn hydrate<const FROM_SERVER: bool>(
self,
cursor: &Cursor<Rndr>,
cursor: &Cursor,
position: &PositionState,
) -> Self::State {
(self.0)().hydrate::<FROM_SERVER>(cursor, position)

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