mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 13:24:05 -05:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
764b9cd57d | ||
|
|
6de2b4006a | ||
|
|
65940cbefa | ||
|
|
8f5c34de8a | ||
|
|
4faa340ba8 | ||
|
|
5af5fdeeed | ||
|
|
9dd52e6c15 | ||
|
|
8535a10bd7 | ||
|
|
7864a12967 | ||
|
|
9733cdcfe1 | ||
|
|
1aaa716dfc | ||
|
|
779b2f2a9f | ||
|
|
72e0abc75c | ||
|
|
a7a8970150 | ||
|
|
2e09f3d102 | ||
|
|
e6fe7fef07 | ||
|
|
629f4f9d0f | ||
|
|
ff5b612e12 | ||
|
|
61571ed24b | ||
|
|
4f3a26ce88 | ||
|
|
83a848b5ec | ||
|
|
eec9edf517 | ||
|
|
861dcf354c | ||
|
|
af3d6cba22 | ||
|
|
a0d657f9b1 | ||
|
|
cddb24ebd3 | ||
|
|
e8afd11995 | ||
|
|
4d01d95175 | ||
|
|
9bf5b22633 | ||
|
|
da4a7d5285 | ||
|
|
2af6c6353c | ||
|
|
7f4b5eb4d1 | ||
|
|
fbf46ca58c |
2
.github/workflows/autofix.yml
vendored
2
.github/workflows/autofix.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
autofix:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with: {toolchain: "nightly-2025-07-16", components: "rustfmt, clippy", target: "wasm32-unknown-unknown", rustflags: ""}
|
||||
- name: Install Glib
|
||||
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -63,6 +63,6 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libglib2.0-dev
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
- name: Semver Checks
|
||||
uses: obi1kenobi/cargo-semver-checks-action@v2
|
||||
|
||||
2
.github/workflows/get-example-changed.yml
vendored
2
.github/workflows/get-example-changed.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
matrix: ${{ steps.set-example-changed.outputs.matrix }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Get example files that changed
|
||||
|
||||
2
.github/workflows/get-examples-matrix.yml
vendored
2
.github/workflows/get-examples-matrix.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
EXCLUDED_EXAMPLES: cargo-make
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
- name: Install jq
|
||||
run: sudo apt-get install jq
|
||||
- name: Set Matrix
|
||||
|
||||
2
.github/workflows/get-leptos-changed.yml
vendored
2
.github/workflows/get-leptos-changed.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
leptos_changed: ${{ steps.set-source-changed.outputs.leptos_changed }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Get source files that changed
|
||||
|
||||
2
.github/workflows/get-leptos-matrix.yml
vendored
2
.github/workflows/get-leptos-matrix.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
- name: Install jq
|
||||
run: sudo apt-get install jq
|
||||
- name: Set Matrix
|
||||
|
||||
2
.github/workflows/publish-book.yml
vendored
2
.github/workflows/publish-book.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
contents: write # To push a branch
|
||||
pull-requests: write # To create a PR from that branch
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install mdbook
|
||||
|
||||
2
.github/workflows/run-cargo-make-task.yml
vendored
2
.github/workflows/run-cargo-make-task.yml
vendored
@@ -53,7 +53,7 @@ jobs:
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libglib2.0-dev
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
|
||||
919
Cargo.lock
generated
919
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
120
Cargo.toml
120
Cargo.toml
@@ -2,7 +2,6 @@
|
||||
resolver = "2"
|
||||
members = [
|
||||
# utilities
|
||||
"oco",
|
||||
"any_spawner",
|
||||
"const_str_slice_concat",
|
||||
"either_of",
|
||||
@@ -50,46 +49,45 @@ any_spawner = { path = "./any_spawner/", version = "0.3.0" }
|
||||
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1" }
|
||||
either_of = { path = "./either_of/", version = "0.1.6" }
|
||||
hydration_context = { path = "./hydration_context", version = "0.3.0" }
|
||||
leptos = { path = "./leptos", version = "0.8.11" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.8.7" }
|
||||
leptos = { path = "./leptos", version = "0.8.15" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.8.8" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.8.7" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.5" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.8.6" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.8.10" }
|
||||
leptos_router = { path = "./router", version = "0.8.9" }
|
||||
leptos_router_macro = { path = "./router_macro", version = "0.8.5" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.8.5" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.8.7" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.8.13" }
|
||||
leptos_router = { path = "./router", version = "0.8.11" }
|
||||
leptos_router_macro = { path = "./router_macro", version = "0.8.6" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.8.6" }
|
||||
leptos_meta = { path = "./meta", version = "0.8.5" }
|
||||
next_tuple = { path = "./next_tuple", version = "0.1.0" }
|
||||
oco_ref = { path = "./oco", version = "0.2.1" }
|
||||
or_poisoned = { path = "./or_poisoned", version = "0.1.0" }
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.2.9" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.3.0" }
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.2.12" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.3.1" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.2.6" }
|
||||
server_fn = { path = "./server_fn", version = "0.8.8" }
|
||||
server_fn = { path = "./server_fn", version = "0.8.9" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.8.8" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.5" }
|
||||
tachys = { path = "./tachys", version = "0.2.10" }
|
||||
wasm_split_helpers = { path = "./wasm_split", version = "0.1.2" }
|
||||
wasm_split_macros = { path = "./wasm_split_macros", version = "0.1.3" }
|
||||
tachys = { path = "./tachys", version = "0.2.11" }
|
||||
|
||||
# members deps
|
||||
async-once-cell = { default-features = false, version = "0.5.3" }
|
||||
async-once-cell = { default-features = false, version = "0.5.4" }
|
||||
itertools = { default-features = false, version = "0.14.0" }
|
||||
convert_case = { default-features = false, version = "0.8.0" }
|
||||
serde_json = { default-features = false, version = "1.0.143" }
|
||||
trybuild = { default-features = false, version = "1.0.110" }
|
||||
typed-builder = { default-features = false, version = "0.21.2" }
|
||||
convert_case = { default-features = false, version = "0.10.0" }
|
||||
serde_json = { default-features = false, version = "1.0.145" }
|
||||
trybuild = { default-features = false, version = "1.0.114" }
|
||||
typed-builder = { default-features = false, version = "0.23.2" }
|
||||
typed-builder-macro = { default-features = false, version = "0.23.2" }
|
||||
thiserror = { default-features = false, version = "2.0.17" }
|
||||
wasm-bindgen = { default-features = false, version = "0.2.100" }
|
||||
indexmap = { default-features = false, version = "2.11.0" }
|
||||
wasm-bindgen = { default-features = false, version = "0.2.106" }
|
||||
indexmap = { default-features = false, version = "2.12.1" }
|
||||
rstml = { default-features = false, version = "0.12.1" }
|
||||
rustc_version = { default-features = false, version = "0.4.1" }
|
||||
guardian = { default-features = false, version = "1.3.0" }
|
||||
rustc-hash = { default-features = false, version = "2.1.1" }
|
||||
actix-web = { default-features = false, version = "4.11.0" }
|
||||
tracing = { default-features = false, version = "0.1.41" }
|
||||
slotmap = { default-features = false, version = "1.0.7" }
|
||||
actix-web = { default-features = false, version = "4.12.1" }
|
||||
tracing = { default-features = false, version = "0.1.43" }
|
||||
slotmap = { default-features = false, version = "1.1.1" }
|
||||
futures = { default-features = false, version = "0.3.31" }
|
||||
dashmap = { default-features = false, version = "6.1.0" }
|
||||
pin-project-lite = { default-features = false, version = "0.2.16" }
|
||||
@@ -99,42 +97,41 @@ html-escape = { default-features = false, version = "0.2.13" }
|
||||
proc-macro-error2 = { default-features = false, version = "2.0.1" }
|
||||
const_format = { default-features = false, version = "0.2.35" }
|
||||
gloo-net = { default-features = false, version = "0.6.0" }
|
||||
url = { default-features = false, version = "2.5.4" }
|
||||
tokio = { default-features = false, version = "1.47.1" }
|
||||
url = { default-features = false, version = "2.5.7" }
|
||||
tokio = { default-features = false, version = "1.48.0" }
|
||||
base64 = { default-features = false, version = "0.22.1" }
|
||||
cfg-if = { default-features = false, version = "1.0.3" }
|
||||
wasm-bindgen-futures = { default-features = false, version = "0.4.50" }
|
||||
cfg-if = { default-features = false, version = "1.0.4" }
|
||||
wasm-bindgen-futures = { default-features = false, version = "0.4.56" }
|
||||
tower = { default-features = false, version = "0.5.2" }
|
||||
proc-macro2 = { default-features = false, version = "1.0.101" }
|
||||
serde = { default-features = false, version = "1.0.219" }
|
||||
proc-macro2 = { default-features = false, version = "1.0.103" }
|
||||
serde = { default-features = false, version = "1.0.228" }
|
||||
parking_lot = { default-features = false, version = "0.12.5" }
|
||||
axum = { default-features = false, version = "0.8.6" }
|
||||
axum = { default-features = false, version = "0.8.7" }
|
||||
serde_qs = { default-features = false, version = "0.15.0" }
|
||||
syn = { default-features = false, version = "2.0.106" }
|
||||
syn = { default-features = false, version = "2.0.111" }
|
||||
xxhash-rust = { default-features = false, version = "0.8.15" }
|
||||
paste = { default-features = false, version = "1.0.15" }
|
||||
quote = { default-features = false, version = "1.0.41" }
|
||||
web-sys = { default-features = false, version = "0.3.77" }
|
||||
js-sys = { default-features = false, version = "0.3.77" }
|
||||
rand = { default-features = false, version = "0.9.1" }
|
||||
serde-lite = { default-features = false, version = "0.5.0" }
|
||||
quote = { default-features = false, version = "1.0.42" }
|
||||
web-sys = { default-features = false, version = "0.3.83" }
|
||||
js-sys = { default-features = false, version = "0.3.83" }
|
||||
rand = { default-features = false, version = "0.9.2" }
|
||||
serde-lite = { default-features = false, version = "0.5.1" }
|
||||
tokio-tungstenite = { default-features = false, version = "0.28.0" }
|
||||
serial_test = { default-features = false, version = "3.2.0" }
|
||||
erased = { default-features = false, version = "0.1.2" }
|
||||
glib = { default-features = false, version = "0.20.12" }
|
||||
glib = { default-features = false, version = "0.21.5" }
|
||||
async-trait = { default-features = false, version = "0.1.89" }
|
||||
typed-builder-macro = { default-features = false, version = "0.21.0" }
|
||||
linear-map = { default-features = false, version = "1.2.0" }
|
||||
anyhow = { default-features = false, version = "1.0.100" }
|
||||
walkdir = { default-features = false, version = "2.5.0" }
|
||||
actix-ws = { default-features = false, version = "0.3.0" }
|
||||
tower-http = { default-features = false, version = "0.6.4" }
|
||||
tower-http = { default-features = false, version = "0.6.8" }
|
||||
prettyplease = { default-features = false, version = "0.2.37" }
|
||||
inventory = { default-features = false, version = "0.3.21" }
|
||||
config = { default-features = false, version = "0.15.14" }
|
||||
camino = { default-features = false, version = "1.2.1" }
|
||||
config = { default-features = false, version = "0.15.19" }
|
||||
camino = { default-features = false, version = "1.2.2" }
|
||||
ciborium = { default-features = false, version = "0.2.2" }
|
||||
bitcode = { default-features = false, version = "0.6.6" }
|
||||
bitcode = { default-features = false, version = "0.6.7" }
|
||||
multer = { default-features = false, version = "3.1.0" }
|
||||
leptos-spin-macro = { default-features = false, version = "0.2.0" }
|
||||
sledgehammer_utils = { default-features = false, version = "0.3.1" }
|
||||
@@ -142,38 +139,39 @@ sledgehammer_bindgen = { default-features = false, version = "0.6.0" }
|
||||
wasm-streams = { default-features = false, version = "0.4.2" }
|
||||
rkyv = { default-features = false, version = "0.8.12" }
|
||||
temp-env = { default-features = false, version = "0.3.6" }
|
||||
uuid = { default-features = false, version = "1.18.0" }
|
||||
bytes = { default-features = false, version = "1.10.1" }
|
||||
http = { default-features = false, version = "1.3.1" }
|
||||
regex = { default-features = false, version = "1.11.3" }
|
||||
uuid = { default-features = false, version = "1.19.0" }
|
||||
bytes = { default-features = false, version = "1.11.0" }
|
||||
http = { default-features = false, version = "1.4.0" }
|
||||
regex = { default-features = false, version = "1.12.2" }
|
||||
drain_filter_polyfill = { default-features = false, version = "0.1.3" }
|
||||
tempfile = { default-features = false, version = "3.23.0" }
|
||||
futures-lite = { default-features = false, version = "2.6.1" }
|
||||
log = { default-features = false, version = "0.4.27" }
|
||||
log = { default-features = false, version = "0.4.29" }
|
||||
percent-encoding = { default-features = false, version = "2.3.2" }
|
||||
async-executor = { default-features = false, version = "1.13.2" }
|
||||
const-str = { default-features = false, version = "0.6.4" }
|
||||
async-executor = { default-features = false, version = "1.13.3" }
|
||||
const-str = { default-features = false, version = "0.7.1" }
|
||||
http-body-util = { default-features = false, version = "0.1.3" }
|
||||
hyper = { default-features = false, version = "1.7.0" }
|
||||
hyper = { default-features = false, version = "1.8.1" }
|
||||
postcard = { default-features = false, version = "1.1.3" }
|
||||
rmp-serde = { default-features = false, version = "1.3.0" }
|
||||
reqwest = { default-features = false, version = "0.12.23" }
|
||||
reqwest = { default-features = false, version = "0.12.26" }
|
||||
tower-layer = { default-features = false, version = "0.3.3" }
|
||||
attribute-derive = { default-features = false, version = "0.10.5" }
|
||||
insta = { default-features = false, version = "1.43.1" }
|
||||
codee = { default-features = false, version = "0.3.0" }
|
||||
insta = { default-features = false, version = "1.44.3" }
|
||||
codee = { default-features = false, version = "0.3.5" }
|
||||
actix-http = { default-features = false, version = "3.11.2" }
|
||||
wasm-bindgen-test = { default-features = false, version = "0.3.50" }
|
||||
wasm-bindgen-test = { default-features = false, version = "0.3.56" }
|
||||
rustversion = { default-features = false, version = "1.0.22" }
|
||||
getrandom = { default-features = false, version = "0.3.3" }
|
||||
actix-files = { default-features = false, version = "0.6.6" }
|
||||
getrandom = { default-features = false, version = "0.3.4" }
|
||||
actix-files = { default-features = false, version = "0.6.9" }
|
||||
async-lock = { default-features = false, version = "3.4.1" }
|
||||
base16 = { default-features = false, version = "0.2.1" }
|
||||
digest = { default-features = false, version = "0.10.7" }
|
||||
sha2 = { default-features = false, version = "0.10.8" }
|
||||
subsecond = { default-features = false, version = "0.7.0-rc.0" }
|
||||
dioxus-cli-config = { default-features = false, version = "0.7.0-rc.0" }
|
||||
dioxus-devtools = { default-features = false, version = "0.7.0-rc.0" }
|
||||
sha2 = { default-features = false, version = "0.10.9" }
|
||||
subsecond = { default-features = false, version = "0.7.2" }
|
||||
dioxus-cli-config = { default-features = false, version = "0.7.2" }
|
||||
dioxus-devtools = { default-features = false, version = "0.7.2" }
|
||||
wasm_split_helpers = { default-features = false, version = "0.2.0" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -27,7 +27,7 @@ tokio = { version = "1.39", features = [
|
||||
], optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
wasm-bindgen = "0.2.92"
|
||||
wasm-bindgen = "0.2.105"
|
||||
web-sys = { version = "0.3.69", features = [
|
||||
"AddEventListenerOptions",
|
||||
"Document",
|
||||
|
||||
@@ -510,11 +510,9 @@ if (window.hljs) {
|
||||
});
|
||||
view! {
|
||||
<pre><code class="language-rust">{code.await}</code></pre>
|
||||
{
|
||||
move || script.get().map(|script| {
|
||||
view! { <Script>{script}</Script> }
|
||||
})
|
||||
}
|
||||
<ShowLet some=script let:script>
|
||||
<Script>{script}</Script>
|
||||
</ShowLet>
|
||||
}
|
||||
})
|
||||
};
|
||||
@@ -567,11 +565,9 @@ if (window.hljs) {
|
||||
});
|
||||
view! {
|
||||
<pre><code class="language-rust">{code.await}</code></pre>
|
||||
{
|
||||
move || script.get().map(|script| {
|
||||
view! { <Script>{script}</Script> }
|
||||
})
|
||||
}
|
||||
<ShowLet some=script let:script>
|
||||
<Script>{script}</Script>
|
||||
</ShowLet>
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ log = "0.4.22"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
gloo-net = { version = "0.6.0", features = ["http"] }
|
||||
reqwest = { version = "0.12.5", features = ["json"] }
|
||||
wasm-bindgen = "0.2.93"
|
||||
wasm-bindgen = "0.2.105"
|
||||
web-sys = { version = "0.3.70", features = ["AbortController", "AbortSignal"] }
|
||||
send_wrapper = "0.6.0"
|
||||
|
||||
@@ -46,12 +46,12 @@ denylist = ["actix-files", "actix-web", "leptos_actix"]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"], []]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
output-name = "hackernews"
|
||||
# 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.css"
|
||||
|
||||
@@ -145,14 +145,11 @@ fn Story(story: api::Story) -> impl IntoView {
|
||||
Either::Left(
|
||||
view! {
|
||||
<span>
|
||||
{"by "}
|
||||
{story
|
||||
.user
|
||||
.map(|user| {
|
||||
view! {
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
}
|
||||
})} {format!(" {} | ", story.time_ago)}
|
||||
"by "
|
||||
<ShowLet some=story.user let:user>
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
</ShowLet>
|
||||
{format!(" {} | ", story.time_ago)}
|
||||
<A href=format!(
|
||||
"/stories/{}",
|
||||
story.id,
|
||||
|
||||
@@ -30,17 +30,13 @@ pub fn Story() -> impl IntoView {
|
||||
<h1>{story.title}</h1>
|
||||
</a>
|
||||
<span class="host">"(" {story.domain} ")"</span>
|
||||
{story
|
||||
.user
|
||||
.map(|user| {
|
||||
view! {
|
||||
<p class="meta">
|
||||
{story.points} " points | by "
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
{format!(" {}", story.time_ago)}
|
||||
</p>
|
||||
}
|
||||
})}
|
||||
<ShowLet some=story.user let:user>
|
||||
<p class="meta">
|
||||
{story.points} " points | by "
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
{format!(" {}", story.time_ago)}
|
||||
</p>
|
||||
</ShowLet>
|
||||
</div>
|
||||
<div class="item-view-comments">
|
||||
<p class="item-view-comments-header">
|
||||
|
||||
@@ -26,7 +26,7 @@ tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
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"
|
||||
wasm-bindgen = "0.2.105"
|
||||
send_wrapper = { version = "0.6.0", features = ["futures"] }
|
||||
|
||||
[features]
|
||||
|
||||
@@ -133,7 +133,9 @@ fn Story(story: api::Story) -> impl IntoView {
|
||||
Either::Left(view! {
|
||||
<span>
|
||||
{"by "}
|
||||
{story.user.map(|user| view ! { <A href=format!("/users/{user}")>{user.clone()}</A>})}
|
||||
<ShowLet some=story.user let:user>
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
</ShowLet>
|
||||
{format!(" {} | ", story.time_ago)}
|
||||
<A href=format!("/stories/{}", story.id)>
|
||||
{if story.comments_count.unwrap_or_default() > 0 {
|
||||
|
||||
@@ -40,18 +40,20 @@ impl LazyRoute for StoryRoute {
|
||||
<Meta name="description" content=story.title.clone()/>
|
||||
<div class="item-view">
|
||||
<div class="item-view-header">
|
||||
<a href=story.url target="_blank">
|
||||
<h1>{story.title}</h1>
|
||||
</a>
|
||||
<span class="host">
|
||||
"("{story.domain}")"
|
||||
</span>
|
||||
{story.user.map(|user| view! { <p class="meta">
|
||||
{story.points}
|
||||
" points | by "
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
{format!(" {}", story.time_ago)}
|
||||
</p>})}
|
||||
<a href=story.url target="_blank">
|
||||
<h1>{story.title}</h1>
|
||||
</a>
|
||||
<span class="host">
|
||||
"("{story.domain}")"
|
||||
</span>
|
||||
<ShowLet some=story.user let:user>
|
||||
<p class="meta">
|
||||
{story.points}
|
||||
" points | by "
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
{format!(" {}", story.time_ago)}
|
||||
</p>
|
||||
</ShowLet>
|
||||
</div>
|
||||
<div class="item-view-comments">
|
||||
<p class="item-view-comments-header">
|
||||
|
||||
@@ -143,8 +143,10 @@ fn Story(story: api::Story) -> impl IntoView {
|
||||
{if story.story_type != "job" {
|
||||
Either::Left(view! {
|
||||
<span>
|
||||
{"by "}
|
||||
{story.user.map(|user| view ! { <A href=format!("/users/{user}")>{user.clone()}</A>})}
|
||||
"by "
|
||||
<ShowLet some=story.user let:user>
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
</ShowLet>
|
||||
{format!(" {} | ", story.time_ago)}
|
||||
<A href=format!("/stories/{}", story.id)>
|
||||
{if story.comments_count.unwrap_or_default() > 0 {
|
||||
|
||||
@@ -32,18 +32,20 @@ pub fn Story() -> impl IntoView {
|
||||
<Meta name="description" content=story.title.clone()/>
|
||||
<div class="item-view">
|
||||
<div class="item-view-header">
|
||||
<a href=story.url target="_blank">
|
||||
<h1>{story.title}</h1>
|
||||
</a>
|
||||
<span class="host">
|
||||
"("{story.domain}")"
|
||||
</span>
|
||||
{story.user.map(|user| view! { <p class="meta">
|
||||
{story.points}
|
||||
" points | by "
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
{format!(" {}", story.time_ago)}
|
||||
</p>})}
|
||||
<a href=story.url target="_blank">
|
||||
<h1>{story.title}</h1>
|
||||
</a>
|
||||
<span class="host">
|
||||
"("{story.domain}")"
|
||||
</span>
|
||||
<ShowLet some=story.user let:user>
|
||||
<p class="meta">
|
||||
{story.points}
|
||||
" points | by "
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
{format!(" {}", story.time_ago)}
|
||||
</p>
|
||||
</ShowLet>
|
||||
</div>
|
||||
<div class="item-view-comments">
|
||||
<p class="item-view-comments-header">
|
||||
|
||||
@@ -139,14 +139,11 @@ fn Story(story: api::Story) -> impl IntoView {
|
||||
Either::Left(
|
||||
view! {
|
||||
<span>
|
||||
{"by "}
|
||||
{story
|
||||
.user
|
||||
.map(|user| {
|
||||
view! {
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
}
|
||||
})} {format!(" {} | ", story.time_ago)}
|
||||
"by "
|
||||
<ShowLet some=story.user let:user>
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
</ShowLet>
|
||||
{format!(" {} | ", story.time_ago)}
|
||||
<A href=format!(
|
||||
"/stories/{}",
|
||||
story.id,
|
||||
|
||||
@@ -35,17 +35,13 @@ pub fn Story() -> impl IntoView {
|
||||
<h1>{story.title}</h1>
|
||||
</a>
|
||||
<span class="host">"("{story.domain}")"</span>
|
||||
{story
|
||||
.user
|
||||
.map(|user| {
|
||||
view! {
|
||||
<p class="meta">
|
||||
{story.points} " points | by "
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
{format!(" {}", story.time_ago)}
|
||||
</p>
|
||||
}
|
||||
})}
|
||||
<ShowLet some=story.user let:user>
|
||||
<p class="meta">
|
||||
{story.points} " points | by "
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
{format!(" {}", story.time_ago)}
|
||||
</p>
|
||||
</ShowLet>
|
||||
</div>
|
||||
<div class="item-view-comments">
|
||||
<p class="item-view-comments-header">
|
||||
|
||||
@@ -9,6 +9,3 @@ routing when you use islands.
|
||||
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.
|
||||
|
||||
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`.
|
||||
|
||||
@@ -5,4 +5,4 @@ test cases that typically happens at integration.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Run `cargo leptos watch` to run this example.
|
||||
Run `cargo leptos watch --split` to run this example.
|
||||
|
||||
@@ -17,7 +17,7 @@ leptos_router = { path = "../../router" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
thiserror = "2.0.12"
|
||||
tokio = { version = "1.39", features = [ "rt-multi-thread", "macros", "time" ], optional = true }
|
||||
wasm-bindgen = "0.2.92"
|
||||
wasm-bindgen = "0.2.106"
|
||||
|
||||
[features]
|
||||
hydrate = [
|
||||
|
||||
38
examples/regression/e2e/features/issue_4492.feature
Normal file
38
examples/regression/e2e/features/issue_4492.feature
Normal file
@@ -0,0 +1,38 @@
|
||||
@check_issue_4492
|
||||
Feature: Regression test for issue #4492
|
||||
|
||||
Scenario: Scenario A should show Loading once on first load.
|
||||
Given I see the app
|
||||
And I can access regression test 4492
|
||||
When I click the button a-toggle
|
||||
Then I see a-result has the text Loading...
|
||||
When I wait 100ms
|
||||
Then I see a-result has the text 0
|
||||
When I click the button a-button
|
||||
Then I see a-result has the text 0
|
||||
When I wait 100ms
|
||||
Then I see a-result has the text 1
|
||||
|
||||
Scenario: Scenario B should never show Loading
|
||||
Given I see the app
|
||||
And I can access regression test 4492
|
||||
When I click the button b-toggle
|
||||
Then I see b-result has the text 0
|
||||
When I click the button b-button
|
||||
Then I see b-result has the text 0
|
||||
When I wait 100ms
|
||||
Then I see b-result has the text 1
|
||||
When I click the button b-button
|
||||
Then I see b-result has the text 1
|
||||
When I wait 100ms
|
||||
Then I see b-result has the text 2
|
||||
|
||||
Scenario: Scenario C should never show Loading
|
||||
Given I see the app
|
||||
And I can access regression test 4492
|
||||
When I click the button c-toggle
|
||||
Then I see c-result has the text 0
|
||||
When I click the button c-button
|
||||
Then I see c-result has the text 42
|
||||
When I wait 100ms
|
||||
Then I see c-result has the text 1
|
||||
@@ -15,3 +15,9 @@ pub async fn click_link(client: &Client, text: &str) -> Result<()> {
|
||||
link.click().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn click_button(client: &Client, id: &str) -> Result<()> {
|
||||
let btn = find::element_by_id(&client, &id).await?;
|
||||
btn.click().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
10
examples/regression/e2e/tests/fixtures/check.rs
vendored
10
examples/regression/e2e/tests/fixtures/check.rs
vendored
@@ -7,7 +7,15 @@ pub async fn result_text_is(
|
||||
client: &Client,
|
||||
expected_text: &str,
|
||||
) -> Result<()> {
|
||||
let actual = find::text_at_id(client, "result").await?;
|
||||
element_text_is(client, "result", expected_text).await
|
||||
}
|
||||
|
||||
pub async fn element_text_is(
|
||||
client: &Client,
|
||||
id: &str,
|
||||
expected_text: &str,
|
||||
) -> Result<()> {
|
||||
let actual = find::text_at_id(client, id).await?;
|
||||
assert_eq!(&actual, expected_text);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -20,6 +20,14 @@ async fn i_select_the_link(world: &mut AppWorld, text: String) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[when(regex = "^I click the button (.*)$")]
|
||||
async fn i_click_the_button(world: &mut AppWorld, id: String) -> Result<()> {
|
||||
let client = &world.client;
|
||||
action::click_button(client, &id).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[given(expr = "I select the following links")]
|
||||
#[when(expr = "I select the following links")]
|
||||
async fn i_select_the_following_links(
|
||||
@@ -54,3 +62,10 @@ async fn i_go_back(world: &mut AppWorld) -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[when(regex = r"^I wait (\d+)ms$")]
|
||||
async fn i_wait_ms(_world: &mut AppWorld, ms: u64) -> Result<()> {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(ms)).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -19,6 +19,17 @@ async fn i_see_the_result_is_the_string(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[then(regex = r"^I see ([\w-]+) has the text (.*)$")]
|
||||
async fn i_see_element_has_text(
|
||||
world: &mut AppWorld,
|
||||
id: String,
|
||||
text: String,
|
||||
) -> Result<()> {
|
||||
let client = &world.client;
|
||||
check::element_text_is(client, &id, &text).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[then(regex = r"^I see the navbar$")]
|
||||
async fn i_see_the_navbar(world: &mut AppWorld) -> Result<()> {
|
||||
let client = &world.client;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
issue_4005::Routes4005, issue_4088::Routes4088, issue_4217::Routes4217,
|
||||
issue_4285::Routes4285, issue_4296::Routes4296, issue_4324::Routes4324,
|
||||
pr_4015::Routes4015, pr_4091::Routes4091,
|
||||
issue_4492::Routes4492, pr_4015::Routes4015, pr_4091::Routes4091,
|
||||
};
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::{MetaTags, *};
|
||||
@@ -48,6 +48,7 @@ pub fn App() -> impl IntoView {
|
||||
<Routes4285/>
|
||||
<Routes4296/>
|
||||
<Routes4324/>
|
||||
<Routes4492/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
@@ -75,6 +76,7 @@ fn HomePage() -> impl IntoView {
|
||||
<li><a href="/4285/">"4285"</a></li>
|
||||
<li><a href="/4296/">"4296"</a></li>
|
||||
<li><a href="/4324/">"4324"</a></li>
|
||||
<li><a href="/4492/">"4492"</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
}
|
||||
|
||||
114
examples/regression/src/issue_4492.rs
Normal file
114
examples/regression/src/issue_4492.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
use leptos::prelude::*;
|
||||
#[allow(unused_imports)]
|
||||
use leptos_router::{
|
||||
components::Route, path, MatchNestedRoutes, NavigateOptions,
|
||||
};
|
||||
|
||||
#[component]
|
||||
pub fn Routes4492() -> impl MatchNestedRoutes + Clone {
|
||||
view! {
|
||||
<Route path=path!("4492") view=Issue4492/>
|
||||
}
|
||||
.into_inner()
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Issue4492() -> impl IntoView {
|
||||
let show_a = RwSignal::new(false);
|
||||
let show_b = RwSignal::new(false);
|
||||
let show_c = RwSignal::new(false);
|
||||
|
||||
view! {
|
||||
<button id="a-toggle" on:click=move |_| show_a.set(!show_a.get())>"Toggle A"</button>
|
||||
<button id="b-toggle" on:click=move |_| show_b.set(!show_b.get())>"Toggle B"</button>
|
||||
<button id="c-toggle" on:click=move |_| show_c.set(!show_c.get())>"Toggle C"</button>
|
||||
|
||||
<Show when=move || show_a.get()>
|
||||
<ScenarioA/>
|
||||
</Show>
|
||||
<Show when=move || show_b.get()>
|
||||
<ScenarioB/>
|
||||
</Show>
|
||||
<Show when=move || show_c.get()>
|
||||
<ScenarioC/>
|
||||
</Show>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ScenarioA() -> impl IntoView {
|
||||
// scenario A: one truly-async resource is read on click
|
||||
let counter = RwSignal::new(0);
|
||||
let resource = Resource::new(
|
||||
move || counter.get(),
|
||||
|count| async move {
|
||||
sleep(50).await.unwrap();
|
||||
count
|
||||
},
|
||||
);
|
||||
view! {
|
||||
<Transition fallback=|| view! { <p id="a-result">"Loading..."</p> }>
|
||||
<p id="a-result">{resource}</p>
|
||||
</Transition>
|
||||
<button id="a-button" on:click=move |_| *counter.write() += 1>"+1"</button>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ScenarioB() -> impl IntoView {
|
||||
// scenario B: resource immediately available first time, then after 250ms
|
||||
let counter = RwSignal::new(0);
|
||||
let resource = Resource::new(
|
||||
move || counter.get(),
|
||||
|count| async move {
|
||||
if count == 0 {
|
||||
count
|
||||
} else {
|
||||
sleep(50).await.unwrap();
|
||||
count
|
||||
}
|
||||
},
|
||||
);
|
||||
view! {
|
||||
<Transition fallback=|| view! { <p id="b-result">"Loading..."</p> }>
|
||||
<p id="b-result">{resource}</p>
|
||||
</Transition>
|
||||
<button id="b-button" on:click=move |_| *counter.write() += 1>"+1"</button>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ScenarioC() -> impl IntoView {
|
||||
// scenario C: not even a resource on the first run, just a value
|
||||
// see https://github.com/leptos-rs/leptos/issues/3868
|
||||
let counter = RwSignal::new(0);
|
||||
let s_res = StoredValue::new(None::<ArcLocalResource<i32>>);
|
||||
let resource = move || {
|
||||
let count = counter.get();
|
||||
if count == 0 {
|
||||
count
|
||||
} else {
|
||||
let r = s_res.get_value().unwrap_or_else(|| {
|
||||
let res = ArcLocalResource::new(move || async move {
|
||||
sleep(50).await.unwrap();
|
||||
count
|
||||
});
|
||||
s_res.set_value(Some(res.clone()));
|
||||
res
|
||||
});
|
||||
r.get().unwrap_or(42)
|
||||
}
|
||||
};
|
||||
view! {
|
||||
<Transition fallback=|| view! { <p id="c-result">"Loading..."</p> }>
|
||||
<p id="c-result">{resource}</p>
|
||||
</Transition>
|
||||
<button id="c-button" on:click=move |_| *counter.write() += 1>"+1"</button>
|
||||
}
|
||||
}
|
||||
|
||||
#[server]
|
||||
async fn sleep(ms: u64) -> Result<(), ServerFnError> {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(ms)).await;
|
||||
Ok(())
|
||||
}
|
||||
@@ -5,6 +5,7 @@ mod issue_4217;
|
||||
mod issue_4285;
|
||||
mod issue_4296;
|
||||
mod issue_4324;
|
||||
mod issue_4492;
|
||||
mod pr_4015;
|
||||
mod pr_4091;
|
||||
|
||||
|
||||
@@ -440,7 +440,14 @@ pub fn FileUploadWithProgress() -> impl IntoView {
|
||||
let mut entry =
|
||||
FILES.entry(filename.to_string()).or_insert_with(|| {
|
||||
println!("[{filename}]\tinserting channel");
|
||||
let (tx, rx) = broadcast(128);
|
||||
// NOTE: this channel capacity is set arbitrarily for this demo code.
|
||||
// it allows for up to exactly 1048 chunks to be sent, which sets an upper cap
|
||||
// on upload size (the precise details vary by client)
|
||||
// in a real system, you will want to create some more reasonable ways of
|
||||
// sending and sharing notifications
|
||||
//
|
||||
// see https://github.com/leptos-rs/leptos/issues/4397 for related discussion
|
||||
let (tx, rx) = broadcast(1048);
|
||||
File { total: 0, tx, rx }
|
||||
});
|
||||
entry.total += len;
|
||||
@@ -557,17 +564,12 @@ pub fn FileUploadWithProgress() -> impl IntoView {
|
||||
<input type="submit" />
|
||||
</form>
|
||||
{move || filename.get().map(|filename| view! { <p>Uploading {filename}</p> })}
|
||||
{move || {
|
||||
max.get()
|
||||
.map(|max| {
|
||||
view! {
|
||||
<progress
|
||||
max=max
|
||||
value=move || current.get().unwrap_or_default()
|
||||
></progress>
|
||||
}
|
||||
})
|
||||
}}
|
||||
<ShowLet some=max let:max>
|
||||
<progress
|
||||
max=max
|
||||
value=move || current.get().unwrap_or_default()
|
||||
></progress>
|
||||
</ShowLet>
|
||||
}
|
||||
}
|
||||
#[component]
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use axum::Router;
|
||||
use axum::{
|
||||
http::{HeaderName, HeaderValue},
|
||||
Router,
|
||||
};
|
||||
use leptos::{logging::log, prelude::*};
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use ssr_modes_axum::app::*;
|
||||
@@ -17,7 +20,24 @@ async fn main() {
|
||||
let leptos_options = leptos_options.clone();
|
||||
move || shell(leptos_options.clone())
|
||||
})
|
||||
.fallback(leptos_axum::file_and_error_handler(shell))
|
||||
.fallback(leptos_axum::file_and_error_handler_with_context(
|
||||
move || {
|
||||
// if you want to add custom headers to the static file handler response,
|
||||
// you can do that by providing `ResponseOptions` via context
|
||||
let opts = use_context::<leptos_axum::ResponseOptions>()
|
||||
.unwrap_or_default();
|
||||
opts.insert_header(
|
||||
HeaderName::from_static("cross-origin-opener-policy"),
|
||||
HeaderValue::from_static("same-origin"),
|
||||
);
|
||||
opts.insert_header(
|
||||
HeaderName::from_static("cross-origin-embedder-policy"),
|
||||
HeaderValue::from_static("require-corp"),
|
||||
);
|
||||
provide_context(opts);
|
||||
},
|
||||
shell,
|
||||
))
|
||||
.with_state(leptos_options);
|
||||
|
||||
// run our app with hyper
|
||||
|
||||
@@ -663,26 +663,24 @@ impl From<Vec<FieldNavItem>> for FieldNavCtx {
|
||||
#[component]
|
||||
pub fn FieldNavPortlet() -> impl IntoView {
|
||||
let ctx = expect_context::<ReadSignal<Option<FieldNavCtx>>>();
|
||||
move || {
|
||||
let ctx = ctx.get();
|
||||
ctx.map(|ctx| {
|
||||
view! {
|
||||
<div id="FieldNavPortlet">
|
||||
<span>"FieldNavPortlet:"</span>
|
||||
<nav>
|
||||
{ctx
|
||||
.0
|
||||
.map(|ctx| {
|
||||
ctx.into_iter()
|
||||
.map(|FieldNavItem { href, text }| {
|
||||
view! { <A href=href>{text}</A> }
|
||||
})
|
||||
.collect_view()
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
}
|
||||
})
|
||||
|
||||
view! {
|
||||
<ShowLet some=ctx let:ctx>
|
||||
<div id="FieldNavPortlet">
|
||||
<span>"FieldNavPortlet:"</span>
|
||||
<nav>
|
||||
{ctx
|
||||
.0
|
||||
.map(|ctx| {
|
||||
ctx.into_iter()
|
||||
.map(|FieldNavItem { href, text }| {
|
||||
view! { <A href=href>{text}</A> }
|
||||
})
|
||||
.collect_view()
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
</ShowLet>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Axum integrations for the Leptos web framework."
|
||||
version = "0.8.6"
|
||||
version = "0.8.7"
|
||||
rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
|
||||
@@ -2050,7 +2050,20 @@ where
|
||||
let res = res.await.unwrap();
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
let owner = Owner::new();
|
||||
owner.with(|| {
|
||||
additional_context();
|
||||
let res = res.into_response();
|
||||
if let Some(response_options) =
|
||||
use_context::<ResponseOptions>()
|
||||
{
|
||||
let mut res = AxumResponse(res);
|
||||
res.extend_response(&response_options);
|
||||
res.0
|
||||
} else {
|
||||
res
|
||||
}
|
||||
})
|
||||
} else {
|
||||
let mut res = handle_response_inner(
|
||||
move || {
|
||||
|
||||
@@ -4,7 +4,7 @@ authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Utilities to help build server integrations for the Leptos web framework."
|
||||
version = "0.8.6"
|
||||
version = "0.8.7"
|
||||
rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ pub trait ExtendResponse: Sized {
|
||||
// drop the owner, cleaning up the reactive runtime,
|
||||
// once the stream is over
|
||||
.chain(once(async move {
|
||||
owner.unset();
|
||||
owner.unset_with_forced_cleanup();
|
||||
Default::default()
|
||||
})),
|
||||
));
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
[package]
|
||||
name = "leptos"
|
||||
version = "0.8.11"
|
||||
version = "0.8.15"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
homepage = "https://leptos.dev/"
|
||||
description = "Leptos is a full-stack, isomorphic Rust web framework leveraging fine-grained reactivity to build declarative user interfaces."
|
||||
readme = "../README.md"
|
||||
rust-version.workspace = true
|
||||
@@ -57,7 +58,7 @@ serde_qs = { workspace = true, default-features = true }
|
||||
slotmap = { workspace = true, default-features = true }
|
||||
futures = { workspace = true, default-features = true }
|
||||
send_wrapper = { workspace = true, default-features = true }
|
||||
wasm_split_helpers.workspace = true
|
||||
wasm_split_helpers = { workspace = true, default-features = true }
|
||||
subsecond = { workspace = true, default-features = true, optional = true }
|
||||
dioxus-cli-config = { workspace = true, default-features = true, optional = true }
|
||||
dioxus-devtools = { workspace = true, default-features = true, optional = true }
|
||||
|
||||
@@ -203,7 +203,7 @@ pub mod prelude {
|
||||
pub mod form;
|
||||
|
||||
/// A standard way to wrap functions and closures to pass them to components.
|
||||
pub mod callback;
|
||||
pub use reactive_graph::callback;
|
||||
|
||||
/// Types that can be passed as the `children` prop of a component.
|
||||
pub mod children;
|
||||
@@ -365,7 +365,8 @@ pub use serde_json;
|
||||
pub use tracing;
|
||||
#[doc(hidden)]
|
||||
pub use wasm_bindgen;
|
||||
pub use wasm_split_helpers;
|
||||
#[doc(hidden)]
|
||||
pub use wasm_split_helpers as wasm_split;
|
||||
#[doc(hidden)]
|
||||
pub use web_sys;
|
||||
|
||||
|
||||
@@ -160,3 +160,16 @@ where
|
||||
OptionGetter(Arc::new(move || cloned.get()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Marker type for creating an `OptionGetter` from a static value.
|
||||
/// Used so that the compiler doesn't complain about double implementations of the trait `IntoOptionGetter`.
|
||||
pub struct StaticMarker;
|
||||
|
||||
impl<T> IntoOptionGetter<T, StaticMarker> for Option<T>
|
||||
where
|
||||
T: Clone + Send + Sync + 'static,
|
||||
{
|
||||
fn into_option_getter(self) -> OptionGetter<T> {
|
||||
OptionGetter(Arc::new(move || self.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::{
|
||||
use futures::{channel::oneshot, select, FutureExt};
|
||||
use hydration_context::SerializedDataId;
|
||||
use leptos_macro::component;
|
||||
use or_poisoned::OrPoisoned;
|
||||
use reactive_graph::{
|
||||
computed::{
|
||||
suspense::{LocalResourceNotifier, SuspenseContext},
|
||||
@@ -14,10 +15,13 @@ use reactive_graph::{
|
||||
effect::RenderEffect,
|
||||
owner::{provide_context, use_context, Owner},
|
||||
signal::ArcRwSignal,
|
||||
traits::{Dispose, Get, Read, Track, With, WriteValue},
|
||||
traits::{
|
||||
Dispose, Get, Read, ReadUntracked, Track, With, WithUntracked,
|
||||
WriteValue,
|
||||
},
|
||||
};
|
||||
use slotmap::{DefaultKey, SlotMap};
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tachys::{
|
||||
either::Either,
|
||||
html::attribute::{any_attribute::AnyAttribute, Attribute},
|
||||
@@ -118,14 +122,19 @@ where
|
||||
provide_context(SuspenseContext {
|
||||
tasks: tasks.clone(),
|
||||
});
|
||||
let none_pending = ArcMemo::new(move |prev: Option<&bool>| {
|
||||
tasks.track();
|
||||
if prev.is_none() && starts_local {
|
||||
false
|
||||
} else {
|
||||
tasks.with(SlotMap::is_empty)
|
||||
let none_pending = ArcMemo::new({
|
||||
let tasks = tasks.clone();
|
||||
move |prev: Option<&bool>| {
|
||||
tasks.track();
|
||||
if prev.is_none() && starts_local {
|
||||
false
|
||||
} else {
|
||||
tasks.with(SlotMap::is_empty)
|
||||
}
|
||||
}
|
||||
});
|
||||
let has_tasks =
|
||||
Arc::new(move || !tasks.with_untracked(SlotMap::is_empty));
|
||||
|
||||
OwnedView::new(SuspenseBoundary::<false, _, _> {
|
||||
id,
|
||||
@@ -133,6 +142,7 @@ where
|
||||
fallback,
|
||||
children,
|
||||
error_boundary_parent,
|
||||
has_tasks,
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -155,6 +165,7 @@ pub(crate) struct SuspenseBoundary<const TRANSITION: bool, Fal, Chil> {
|
||||
pub fallback: Fal,
|
||||
pub children: Chil,
|
||||
pub error_boundary_parent: Option<ErrorBoundarySuspendedChildren>,
|
||||
pub has_tasks: Arc<dyn Fn() -> bool + Send + Sync>,
|
||||
}
|
||||
|
||||
impl<const TRANSITION: bool, Fal, Chil> Render
|
||||
@@ -191,12 +202,26 @@ where
|
||||
outer_owner.clone(),
|
||||
);
|
||||
|
||||
if let Some(mut state) = prev {
|
||||
let state = if let Some(mut state) = prev {
|
||||
this.rebuild(&mut state);
|
||||
state
|
||||
} else {
|
||||
this.build()
|
||||
};
|
||||
|
||||
if nth_run == 1 && !(self.has_tasks)() {
|
||||
// if this is the first run, and there are no pending resources at this point,
|
||||
// it means that there were no actually-async resources read while rendering the children
|
||||
// this means that we're effectively on the settled second run: none_pending
|
||||
// won't change false => true and cause this to rerender (and therefore increment nth_run)
|
||||
//
|
||||
// we increment it manually here so that future resource changes won't cause the transition fallback
|
||||
// to be displayed for the first time
|
||||
// see https://github.com/leptos-rs/leptos/issues/3868, https://github.com/leptos-rs/leptos/issues/4492
|
||||
nth_run += 1;
|
||||
}
|
||||
|
||||
state
|
||||
})
|
||||
}
|
||||
|
||||
@@ -234,6 +259,7 @@ where
|
||||
fallback,
|
||||
children,
|
||||
error_boundary_parent,
|
||||
has_tasks,
|
||||
} = self;
|
||||
SuspenseBoundary {
|
||||
id,
|
||||
@@ -241,6 +267,7 @@ where
|
||||
fallback,
|
||||
children: children.add_any_attr(attr),
|
||||
error_boundary_parent,
|
||||
has_tasks,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -320,23 +347,66 @@ where
|
||||
|
||||
// walk over the tree of children once to make sure that all resource loads are registered
|
||||
self.children.dry_resolve();
|
||||
let children = Arc::new(Mutex::new(Some(self.children)));
|
||||
|
||||
// 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 let Some(tasks) = tasks.try_read() {
|
||||
if tasks.is_empty() {
|
||||
if let Some(tx) = tasks_tx.take() {
|
||||
// If the receiver has dropped, it means the ScopedFuture has already
|
||||
// dropped, so it doesn't matter if we manage to send this.
|
||||
_ = tx.send(());
|
||||
}
|
||||
if let Some(tx) = notify_error_boundary.take() {
|
||||
_ = tx.send(());
|
||||
let children = Arc::clone(&children);
|
||||
move |double_checking: Option<bool>| {
|
||||
// on the first run, always track the tasks
|
||||
if double_checking.is_none() {
|
||||
tasks.track();
|
||||
}
|
||||
|
||||
if let Some(curr_tasks) = tasks.try_read_untracked() {
|
||||
if curr_tasks.is_empty() {
|
||||
if double_checking == Some(true) {
|
||||
// we have finished loading, and checking the children again told us there are
|
||||
// no more pending tasks. so we can render both the children and the error boundary
|
||||
|
||||
if let Some(tx) = tasks_tx.take() {
|
||||
// If the receiver has dropped, it means the ScopedFuture has already
|
||||
// dropped, so it doesn't matter if we manage to send this.
|
||||
_ = tx.send(());
|
||||
}
|
||||
if let Some(tx) = notify_error_boundary.take() {
|
||||
_ = tx.send(());
|
||||
}
|
||||
} else {
|
||||
// release the read guard on tasks, as we'll be updating it again
|
||||
drop(curr_tasks);
|
||||
// check the children for additional pending tasks
|
||||
// the will catch additional resource reads nested inside a conditional depending on initial resource reads
|
||||
if let Some(children) =
|
||||
children.lock().or_poisoned().as_mut()
|
||||
{
|
||||
children.dry_resolve();
|
||||
}
|
||||
|
||||
if tasks
|
||||
.try_read()
|
||||
.map(|n| n.is_empty())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
// there are no additional pending tasks, and we can simply return
|
||||
if let Some(tx) = tasks_tx.take() {
|
||||
// If the receiver has dropped, it means the ScopedFuture has already
|
||||
// dropped, so it doesn't matter if we manage to send this.
|
||||
_ = tx.send(());
|
||||
}
|
||||
if let Some(tx) = notify_error_boundary.take() {
|
||||
_ = tx.send(());
|
||||
}
|
||||
}
|
||||
|
||||
// tell ourselves that we're just double-checking
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
tasks.track();
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
@@ -362,12 +432,17 @@ where
|
||||
None
|
||||
}
|
||||
_ = tasks_rx => {
|
||||
let children = {
|
||||
let mut children_lock = children.lock().or_poisoned();
|
||||
children_lock.take().expect("children should not be removed until we render here")
|
||||
};
|
||||
|
||||
// if we ran this earlier, reactive reads would always be registered as None
|
||||
// this is fine in the case where we want to use Suspend and .await on some future
|
||||
// but in situations like a <For each=|| some_resource.snapshot()/> we actually
|
||||
// want to be able to 1) synchronously read a resource's value, but still 2) wait
|
||||
// for it to load before we render everything
|
||||
let mut children = Box::pin(self.children.resolve().fuse());
|
||||
let mut children = Box::pin(children.resolve().fuse());
|
||||
|
||||
// we continue racing the children against the "do we have any local
|
||||
// resources?" Future
|
||||
|
||||
@@ -10,10 +10,11 @@ use reactive_graph::{
|
||||
effect::Effect,
|
||||
owner::{provide_context, use_context, Owner},
|
||||
signal::ArcRwSignal,
|
||||
traits::{Get, Set, Track, With},
|
||||
traits::{Get, Set, Track, With, WithUntracked},
|
||||
wrappers::write::SignalSetter,
|
||||
};
|
||||
use slotmap::{DefaultKey, SlotMap};
|
||||
use std::sync::Arc;
|
||||
use tachys::reactive_graph::OwnedView;
|
||||
|
||||
/// If any [`Resource`](crate::prelude::Resource) is read in the `children` of this
|
||||
@@ -104,14 +105,19 @@ where
|
||||
provide_context(SuspenseContext {
|
||||
tasks: tasks.clone(),
|
||||
});
|
||||
let none_pending = ArcMemo::new(move |prev: Option<&bool>| {
|
||||
tasks.track();
|
||||
if prev.is_none() && starts_local {
|
||||
false
|
||||
} else {
|
||||
tasks.with(SlotMap::is_empty)
|
||||
let none_pending = ArcMemo::new({
|
||||
let tasks = tasks.clone();
|
||||
move |prev: Option<&bool>| {
|
||||
tasks.track();
|
||||
if prev.is_none() && starts_local {
|
||||
false
|
||||
} else {
|
||||
tasks.with(SlotMap::is_empty)
|
||||
}
|
||||
}
|
||||
});
|
||||
let has_tasks =
|
||||
Arc::new(move || !tasks.with_untracked(SlotMap::is_empty));
|
||||
if let Some(set_pending) = set_pending {
|
||||
Effect::new_isomorphic({
|
||||
let none_pending = none_pending.clone();
|
||||
@@ -127,6 +133,7 @@ where
|
||||
fallback,
|
||||
children,
|
||||
error_boundary_parent,
|
||||
has_tasks,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -103,6 +103,76 @@ fn test_classes() {
|
||||
assert_eq!(rendered.to_html(), "<div class=\"my big red car\"></div>");
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[test]
|
||||
fn test_class_with_class_directive_merge() {
|
||||
use leptos::prelude::*;
|
||||
|
||||
// class= followed by class: should merge
|
||||
let rendered: View<HtmlElement<_, _, _>> = view! {
|
||||
<div class="foo" class:bar=true></div>
|
||||
};
|
||||
|
||||
assert_eq!(rendered.to_html(), "<div class=\"foo bar\"></div>");
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[test]
|
||||
fn test_solo_class_directive() {
|
||||
use leptos::prelude::*;
|
||||
|
||||
// Solo class: directive should work without class attribute
|
||||
let rendered: View<HtmlElement<_, _, _>> = view! {
|
||||
<div class:foo=true></div>
|
||||
};
|
||||
|
||||
assert_eq!(rendered.to_html(), "<div class=\"foo\"></div>");
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[test]
|
||||
fn test_class_directive_with_static_class() {
|
||||
use leptos::prelude::*;
|
||||
|
||||
// class:foo comes after class= due to macro sorting
|
||||
// The class= clears buffer, then class:foo appends
|
||||
let rendered: View<HtmlElement<_, _, _>> = view! {
|
||||
<div class:foo=true class="bar"></div>
|
||||
};
|
||||
|
||||
// After macro sorting: class="bar" class:foo=true
|
||||
// Expected: "bar foo"
|
||||
assert_eq!(rendered.to_html(), "<div class=\"bar foo\"></div>");
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[test]
|
||||
fn test_global_class_applied() {
|
||||
use leptos::prelude::*;
|
||||
|
||||
// Test that a global class is properly applied
|
||||
let rendered: View<HtmlElement<_, _, _>> = view! { class="global",
|
||||
<div></div>
|
||||
};
|
||||
|
||||
assert_eq!(rendered.to_html(), "<div class=\"global\"></div>");
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[test]
|
||||
fn test_multiple_class_attributes_overwrite() {
|
||||
use leptos::prelude::*;
|
||||
|
||||
// When multiple class attributes are applied, the last one should win (browser behavior)
|
||||
// This simulates what happens when attributes are combined programmatically
|
||||
let el = leptos::html::div().class("first").class("second");
|
||||
|
||||
let html = el.to_html();
|
||||
|
||||
// The second class attribute should overwrite the first
|
||||
assert_eq!(html, "<div class=\"second\"></div>");
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[test]
|
||||
fn ssr_with_styles() {
|
||||
|
||||
@@ -5,7 +5,7 @@ license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Configuration for the Leptos web framework."
|
||||
readme = "../README.md"
|
||||
version = "0.8.7"
|
||||
version = "0.8.8"
|
||||
rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
|
||||
@@ -221,18 +221,15 @@ fn env_w_default(
|
||||
/// An enum that can be used to define the environment Leptos is running in.
|
||||
/// Setting this to the `PROD` variant will not include the WebSocket code for `cargo-leptos` watch mode.
|
||||
/// Defaults to `DEV`.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
|
||||
#[derive(
|
||||
Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, Default,
|
||||
)]
|
||||
pub enum Env {
|
||||
PROD,
|
||||
#[default]
|
||||
DEV,
|
||||
}
|
||||
|
||||
impl Default for Env {
|
||||
fn default() -> Self {
|
||||
Self::DEV
|
||||
}
|
||||
}
|
||||
|
||||
fn env_from_str(input: &str) -> Result<Env, LeptosConfigError> {
|
||||
let sanitized = input.to_lowercase();
|
||||
match sanitized.as_ref() {
|
||||
@@ -279,18 +276,15 @@ impl TryFrom<String> for Env {
|
||||
|
||||
/// An enum that can be used to define the websocket protocol Leptos uses for hotreloading
|
||||
/// Defaults to `ws`.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
|
||||
#[derive(
|
||||
Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, Default,
|
||||
)]
|
||||
pub enum ReloadWSProtocol {
|
||||
#[default]
|
||||
WS,
|
||||
WSS,
|
||||
}
|
||||
|
||||
impl Default for ReloadWSProtocol {
|
||||
fn default() -> Self {
|
||||
Self::WS
|
||||
}
|
||||
}
|
||||
|
||||
fn ws_from_str(input: &str) -> Result<ReloadWSProtocol, LeptosConfigError> {
|
||||
let sanitized = input.to_lowercase();
|
||||
match sanitized.as_ref() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_macro"
|
||||
version = "0.8.10"
|
||||
version = "0.8.14"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
|
||||
@@ -1016,25 +1016,27 @@ struct PropOpt {
|
||||
name: Option<String>,
|
||||
}
|
||||
|
||||
struct TypedBuilderOpts {
|
||||
struct TypedBuilderOpts<'a> {
|
||||
default: bool,
|
||||
default_with_value: Option<syn::Expr>,
|
||||
strip_option: bool,
|
||||
into: bool,
|
||||
ty: &'a Type,
|
||||
}
|
||||
|
||||
impl TypedBuilderOpts {
|
||||
fn from_opts(opts: &PropOpt, is_ty_option: bool) -> Self {
|
||||
impl<'a> TypedBuilderOpts<'a> {
|
||||
fn from_opts(opts: &PropOpt, ty: &'a Type) -> Self {
|
||||
Self {
|
||||
default: opts.optional || opts.optional_no_strip || opts.attrs,
|
||||
default_with_value: opts.default.clone(),
|
||||
strip_option: opts.strip_option || opts.optional && is_ty_option,
|
||||
strip_option: opts.strip_option || opts.optional && is_option(ty),
|
||||
into: opts.into,
|
||||
ty,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedBuilderOpts {
|
||||
impl TypedBuilderOpts<'_> {
|
||||
fn to_serde_tokens(&self) -> TokenStream {
|
||||
let default = if let Some(v) = &self.default_with_value {
|
||||
let v = v.to_token_stream().to_string();
|
||||
@@ -1053,7 +1055,7 @@ impl TypedBuilderOpts {
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for TypedBuilderOpts {
|
||||
impl ToTokens for TypedBuilderOpts<'_> {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let default = if let Some(v) = &self.default_with_value {
|
||||
let v = v.to_token_stream().to_string();
|
||||
@@ -1064,14 +1066,29 @@ impl ToTokens for TypedBuilderOpts {
|
||||
quote! {}
|
||||
};
|
||||
|
||||
let strip_option = if self.strip_option {
|
||||
// If self.strip_option && self.into, then the strip_option will be represented as part of the transform closure.
|
||||
let strip_option = if self.strip_option && !self.into {
|
||||
quote! { strip_option, }
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
|
||||
let into = if self.into {
|
||||
quote! { into, }
|
||||
if !self.strip_option {
|
||||
let ty = &self.ty;
|
||||
quote! {
|
||||
fn transform<__IntoReactiveValueMarker>(value: impl ::leptos::prelude::IntoReactiveValue<#ty, __IntoReactiveValueMarker>) -> #ty {
|
||||
value.into_reactive_value()
|
||||
},
|
||||
}
|
||||
} else {
|
||||
let ty = unwrap_option(self.ty);
|
||||
quote! {
|
||||
fn transform<__IntoReactiveValueMarker>(value: impl ::leptos::prelude::IntoReactiveValue<#ty, __IntoReactiveValueMarker>) -> Option<#ty> {
|
||||
Some(value.into_reactive_value())
|
||||
},
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
@@ -1107,8 +1124,7 @@ fn prop_builder_fields(
|
||||
ty,
|
||||
} = prop;
|
||||
|
||||
let builder_attrs =
|
||||
TypedBuilderOpts::from_opts(prop_opts, is_option(ty));
|
||||
let builder_attrs = TypedBuilderOpts::from_opts(prop_opts, ty);
|
||||
|
||||
let builder_docs = prop_to_doc(prop, PropDocStyle::Inline);
|
||||
|
||||
@@ -1153,8 +1169,7 @@ fn prop_serializer_fields(vis: &Visibility, props: &[Prop]) -> TokenStream {
|
||||
ty,
|
||||
} = prop;
|
||||
|
||||
let builder_attrs =
|
||||
TypedBuilderOpts::from_opts(prop_opts, is_option(ty));
|
||||
let builder_attrs = TypedBuilderOpts::from_opts(prop_opts, ty);
|
||||
let serde_attrs = builder_attrs.to_serde_tokens();
|
||||
|
||||
let PatIdent { ident, by_ref, .. } = &name;
|
||||
|
||||
@@ -2,12 +2,12 @@ use convert_case::{Case, Casing};
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::Ident;
|
||||
use proc_macro_error2::abort;
|
||||
use quote::quote;
|
||||
use quote::{format_ident, quote};
|
||||
use std::{
|
||||
hash::{DefaultHasher, Hash, Hasher},
|
||||
mem,
|
||||
};
|
||||
use syn::{parse_macro_input, ItemFn};
|
||||
use syn::{parse_macro_input, parse_quote, ItemFn, ReturnType, Stmt};
|
||||
|
||||
pub fn lazy_impl(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
let name = if !args.is_empty() {
|
||||
@@ -16,7 +16,7 @@ pub fn lazy_impl(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
None
|
||||
};
|
||||
|
||||
let mut fun = syn::parse::<ItemFn>(s).unwrap_or_else(|e| {
|
||||
let fun = syn::parse::<ItemFn>(s).unwrap_or_else(|e| {
|
||||
abort!(e.span(), "`lazy` can only be used on a function")
|
||||
});
|
||||
|
||||
@@ -47,29 +47,50 @@ pub fn lazy_impl(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
|
||||
let is_wasm = cfg!(feature = "csr") || cfg!(feature = "hydrate");
|
||||
if is_wasm {
|
||||
let mut fun = fun;
|
||||
let mut return_wrapper = None;
|
||||
if was_async {
|
||||
fun.sig.asyncness = None;
|
||||
let ty = match &fun.sig.output {
|
||||
ReturnType::Default => quote! { () },
|
||||
ReturnType::Type(_, ty) => quote! { #ty },
|
||||
};
|
||||
let sync_output: ReturnType = parse_quote! {
|
||||
-> ::std::pin::Pin<::std::boxed::Box<dyn ::std::future::Future<Output = #ty> + ::std::marker::Send + ::std::marker::Sync>>
|
||||
};
|
||||
let async_output = mem::replace(&mut fun.sig.output, sync_output);
|
||||
let stmts = mem::take(&mut fun.block.stmts);
|
||||
fun.block.stmts.push(Stmt::Expr(parse_quote! {
|
||||
::std::boxed::Box::pin(::leptos::__reexports::send_wrapper::SendWrapper::new(async move {
|
||||
#( #stmts )*
|
||||
}))
|
||||
}, None));
|
||||
return_wrapper = Some(quote! {
|
||||
return_wrapper(let future = _; { future.await } #async_output),
|
||||
});
|
||||
}
|
||||
let preload_name = format_ident!("__preload_{}", fun.sig.ident);
|
||||
|
||||
quote! {
|
||||
#[::leptos::wasm_split_helpers::wasm_split(
|
||||
#[::leptos::wasm_split::wasm_split(
|
||||
#unique_name,
|
||||
::leptos::__reexports::send_wrapper
|
||||
wasm_split_path = ::leptos::wasm_split,
|
||||
preload(#[doc(hidden)] #[allow(non_snake_case)] #preload_name),
|
||||
#return_wrapper
|
||||
)]
|
||||
#fun
|
||||
}
|
||||
} else {
|
||||
let mut fun = fun;
|
||||
if !was_async {
|
||||
fun.sig.asyncness = Some(Default::default());
|
||||
}
|
||||
|
||||
let statements = &mut fun.block.stmts;
|
||||
let old_statements = mem::take(statements);
|
||||
statements.push(
|
||||
syn::parse(
|
||||
quote! {
|
||||
::leptos::prefetch_lazy_fn_on_server(#unique_name_str);
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
statements.push(parse_quote! {
|
||||
::leptos::prefetch_lazy_fn_on_server(#unique_name_str);
|
||||
});
|
||||
statements.extend(old_statements);
|
||||
quote! { #fun }
|
||||
}
|
||||
|
||||
@@ -159,25 +159,27 @@ struct PropOpt {
|
||||
pub attrs: bool,
|
||||
}
|
||||
|
||||
struct TypedBuilderOpts {
|
||||
struct TypedBuilderOpts<'a> {
|
||||
default: bool,
|
||||
default_with_value: Option<syn::Expr>,
|
||||
strip_option: bool,
|
||||
into: bool,
|
||||
ty: &'a Type,
|
||||
}
|
||||
|
||||
impl TypedBuilderOpts {
|
||||
pub fn from_opts(opts: &PropOpt, is_ty_option: bool) -> Self {
|
||||
impl<'a> TypedBuilderOpts<'a> {
|
||||
pub fn from_opts(opts: &PropOpt, ty: &'a Type) -> Self {
|
||||
Self {
|
||||
default: opts.optional || opts.optional_no_strip || opts.attrs,
|
||||
default_with_value: opts.default.clone(),
|
||||
strip_option: opts.strip_option || opts.optional && is_ty_option,
|
||||
strip_option: opts.strip_option || opts.optional && is_option(ty),
|
||||
into: opts.into,
|
||||
ty,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for TypedBuilderOpts {
|
||||
impl ToTokens for TypedBuilderOpts<'_> {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let default = if let Some(v) = &self.default_with_value {
|
||||
let v = v.to_token_stream().to_string();
|
||||
@@ -188,14 +190,29 @@ impl ToTokens for TypedBuilderOpts {
|
||||
quote! {}
|
||||
};
|
||||
|
||||
let strip_option = if self.strip_option {
|
||||
// If self.strip_option && self.into, then the strip_option will be represented as part of the transform closure.
|
||||
let strip_option = if self.strip_option && !self.into {
|
||||
quote! { strip_option, }
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
|
||||
let into = if self.into {
|
||||
quote! { into, }
|
||||
if !self.strip_option {
|
||||
let ty = &self.ty;
|
||||
quote! {
|
||||
fn transform<__IntoReactiveValueMarker>(value: impl ::leptos::prelude::IntoReactiveValue<#ty, __IntoReactiveValueMarker>) -> #ty {
|
||||
value.into_reactive_value()
|
||||
},
|
||||
}
|
||||
} else {
|
||||
let ty = unwrap_option(self.ty);
|
||||
quote! {
|
||||
fn transform<__IntoReactiveValueMarker>(value: impl ::leptos::prelude::IntoReactiveValue<#ty, __IntoReactiveValueMarker>) -> Option<#ty> {
|
||||
Some(value.into_reactive_value())
|
||||
},
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
@@ -227,8 +244,7 @@ fn prop_builder_fields(vis: &Visibility, props: &[Prop]) -> TokenStream {
|
||||
ty,
|
||||
} = prop;
|
||||
|
||||
let builder_attrs =
|
||||
TypedBuilderOpts::from_opts(prop_opts, is_option(ty));
|
||||
let builder_attrs = TypedBuilderOpts::from_opts(prop_opts, ty);
|
||||
|
||||
let builder_docs = prop_to_doc(prop, PropDocStyle::Inline);
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ pub(crate) fn component_to_tokens(
|
||||
|
||||
if optional {
|
||||
optional_props.push(quote! {
|
||||
props.#name = { #value }.map(Into::into);
|
||||
props.#name = { #value }.map(::leptos::prelude::IntoReactiveValue::into_reactive_value);
|
||||
})
|
||||
} else {
|
||||
required_props.push(quote! {
|
||||
@@ -176,7 +176,9 @@ pub(crate) fn component_to_tokens(
|
||||
let spreads = (!(spreads.is_empty())).then(|| {
|
||||
if cfg!(feature = "__internal_erase_components") {
|
||||
quote! {
|
||||
.add_any_attr(vec![#(#spreads.into_any_attr(),)*])
|
||||
.add_any_attr({
|
||||
vec![#(::leptos::attr::any_attribute::IntoAnyAttribute::into_any_attr(#spreads),)*]
|
||||
})
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
|
||||
@@ -120,6 +120,124 @@ fn returns_static_lifetime() {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
#[component]
|
||||
pub fn IntoReactiveValueTestComponentSignal(
|
||||
#[prop(into)] arg1: Signal<String>,
|
||||
#[prop(into)] arg2: Signal<String>,
|
||||
#[prop(into)] arg3: Signal<String>,
|
||||
#[prop(into)] arg4: Signal<usize>,
|
||||
#[prop(into)] arg5: Signal<usize>,
|
||||
#[prop(into)] arg6: Signal<usize>,
|
||||
#[prop(into)] arg7: Signal<Option<usize>>,
|
||||
#[prop(into)] arg8: ArcSignal<String>,
|
||||
#[prop(into)] arg9: ArcSignal<String>,
|
||||
#[prop(into)] arg10: ArcSignal<String>,
|
||||
#[prop(into)] arg11: ArcSignal<usize>,
|
||||
#[prop(into)] arg12: ArcSignal<usize>,
|
||||
#[prop(into)] arg13: ArcSignal<usize>,
|
||||
#[prop(into)] arg14: ArcSignal<Option<usize>>,
|
||||
// Optionals:
|
||||
#[prop(into, optional)] arg15: Option<Signal<usize>>,
|
||||
#[prop(into, optional)] arg16_purposely_omitted: Option<Signal<usize>>,
|
||||
#[prop(into, optional)] arg17: Option<Signal<usize>>,
|
||||
#[prop(into, strip_option)] arg18: Option<Signal<usize>>,
|
||||
) -> impl IntoView {
|
||||
move || {
|
||||
view! {
|
||||
<div>
|
||||
<p>{arg1.get()}</p>
|
||||
<p>{arg2.get()}</p>
|
||||
<p>{arg3.get()}</p>
|
||||
<p>{arg4.get()}</p>
|
||||
<p>{arg5.get()}</p>
|
||||
<p>{arg6.get()}</p>
|
||||
<p>{arg7.get()}</p>
|
||||
<p>{arg8.get()}</p>
|
||||
<p>{arg9.get()}</p>
|
||||
<p>{arg10.get()}</p>
|
||||
<p>{arg11.get()}</p>
|
||||
<p>{arg12.get()}</p>
|
||||
<p>{arg13.get()}</p>
|
||||
<p>{arg14.get()}</p>
|
||||
<p>{arg15.get()}</p>
|
||||
<p>{arg16_purposely_omitted.get()}</p>
|
||||
<p>{arg17.get()}</p>
|
||||
<p>{arg18.get()}</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn IntoReactiveValueTestComponentCallback(
|
||||
#[prop(into)] arg1: Callback<(), String>,
|
||||
#[prop(into)] arg2: Callback<usize, String>,
|
||||
#[prop(into)] arg3: Callback<(usize,), String>,
|
||||
#[prop(into)] arg4: Callback<(usize, String), String>,
|
||||
#[prop(into)] arg5: UnsyncCallback<(), String>,
|
||||
#[prop(into)] arg6: UnsyncCallback<usize, String>,
|
||||
#[prop(into)] arg7: UnsyncCallback<(usize,), String>,
|
||||
#[prop(into)] arg8: UnsyncCallback<(usize, String), String>,
|
||||
) -> impl IntoView {
|
||||
move || {
|
||||
view! {
|
||||
<div>
|
||||
<p>{arg1.run(())}</p>
|
||||
<p>{arg2.run(1)}</p>
|
||||
<p>{arg3.run((2,))}</p>
|
||||
<p>{arg4.run((3, "three".into()))}</p>
|
||||
<p>{arg5.run(())}</p>
|
||||
<p>{arg6.run(1)}</p>
|
||||
<p>{arg7.run((2,))}</p>
|
||||
<p>{arg8.run((3, "three".into()))}</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
#[test]
|
||||
fn test_into_reactive_value_signal() {
|
||||
let _ = view! {
|
||||
<IntoReactiveValueTestComponentSignal
|
||||
arg1=move || "I was a reactive closure!"
|
||||
arg2="I was a basic str!"
|
||||
arg3=Signal::stored("I was already a signal!")
|
||||
arg4=move || 2
|
||||
arg5=3
|
||||
arg6=Signal::stored(4)
|
||||
arg7=|| 2
|
||||
arg8=move || "I was a reactive closure!"
|
||||
arg9="I was a basic str!"
|
||||
arg10=ArcSignal::stored("I was already a signal!".to_string())
|
||||
arg11=move || 2
|
||||
arg12=3
|
||||
arg13=ArcSignal::stored(4)
|
||||
arg14=|| 2
|
||||
arg15=|| 2
|
||||
nostrip:arg17=Some(|| 2)
|
||||
arg18=|| 2
|
||||
/>
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_into_reactive_value_callback() {
|
||||
let _ = view! {
|
||||
<IntoReactiveValueTestComponentCallback
|
||||
arg1=|| "I was a callback static str!"
|
||||
arg2=|_n| "I was a callback static str!"
|
||||
arg3=|(_n,)| "I was a callback static str!"
|
||||
arg4=|(_n, _s)| "I was a callback static str!"
|
||||
arg5=|| "I was a callback static str!"
|
||||
arg6=|_n| "I was a callback static str!"
|
||||
arg7=|(_n,)| "I was a callback static str!"
|
||||
arg8=|(_n, _s)| "I was a callback static str!"
|
||||
/>
|
||||
};
|
||||
}
|
||||
|
||||
// an attempt to catch unhygienic macros regression
|
||||
mod macro_hygiene {
|
||||
// To ensure no relative module path to leptos inside macros.
|
||||
@@ -152,12 +270,7 @@ mod macro_hygiene {
|
||||
|
||||
#[component]
|
||||
fn Component() -> impl IntoView {
|
||||
view! {
|
||||
<div>
|
||||
{().into_any()}
|
||||
{()}
|
||||
</div>
|
||||
}
|
||||
view! { <div>{().into_any()} {()}</div> }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_server"
|
||||
version = "0.8.5"
|
||||
version = "0.8.6"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
|
||||
@@ -15,7 +15,7 @@ use codee::{
|
||||
Decoder, Encoder,
|
||||
};
|
||||
use core::{fmt::Debug, marker::PhantomData};
|
||||
use futures::Future;
|
||||
use futures::{Future, FutureExt};
|
||||
use or_poisoned::OrPoisoned;
|
||||
use reactive_graph::{
|
||||
computed::{
|
||||
@@ -258,11 +258,17 @@ where
|
||||
if let Some(suspense_context) = use_context::<SuspenseContext>() {
|
||||
if self.value.read().or_poisoned().is_none() {
|
||||
let handle = suspense_context.task_id();
|
||||
let ready = SpecialNonReactiveFuture::new(self.ready());
|
||||
reactive_graph::spawn(async move {
|
||||
ready.await;
|
||||
drop(handle);
|
||||
});
|
||||
let mut ready =
|
||||
Box::pin(SpecialNonReactiveFuture::new(self.ready()));
|
||||
match ready.as_mut().now_or_never() {
|
||||
Some(_) => drop(handle),
|
||||
None => {
|
||||
reactive_graph::spawn(async move {
|
||||
ready.await;
|
||||
drop(handle);
|
||||
});
|
||||
}
|
||||
}
|
||||
self.suspenses.write().or_poisoned().push(suspense_context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,6 +188,39 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
thread_local! {
|
||||
static RESOURCE_SOURCE_SIGNAL_ACTIVE: AtomicBool = const { AtomicBool::new(false) };
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
/// Returns whether the current thread is currently running a resource source signal.
|
||||
pub fn in_resource_source_signal() -> bool {
|
||||
RESOURCE_SOURCE_SIGNAL_ACTIVE
|
||||
.with(|scope| scope.load(std::sync::atomic::Ordering::Relaxed))
|
||||
}
|
||||
|
||||
/// Set a static to true whilst running the given function.
|
||||
/// [`is_in_effect_scope`] will return true whilst the function is running.
|
||||
fn run_in_resource_source_signal<T>(fun: impl FnOnce() -> T) -> T {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
// For the theoretical nested case, set back to initial value rather than false:
|
||||
let initial = RESOURCE_SOURCE_SIGNAL_ACTIVE.with(|scope| {
|
||||
scope.swap(true, std::sync::atomic::Ordering::Relaxed)
|
||||
});
|
||||
let result = fun();
|
||||
RESOURCE_SOURCE_SIGNAL_ACTIVE.with(|scope| {
|
||||
scope.store(initial, std::sync::atomic::Ordering::Relaxed)
|
||||
});
|
||||
result
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
fun()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> ReadUntracked for ArcResource<T, Ser>
|
||||
where
|
||||
T: 'static,
|
||||
@@ -202,7 +235,9 @@ where
|
||||
computed::suspense::SuspenseContext, effect::in_effect_scope,
|
||||
owner::use_context,
|
||||
};
|
||||
if !in_effect_scope() && use_context::<SuspenseContext>().is_none()
|
||||
if !in_effect_scope()
|
||||
&& !in_resource_source_signal()
|
||||
&& use_context::<SuspenseContext>().is_none()
|
||||
{
|
||||
let location = std::panic::Location::caller();
|
||||
reactive_graph::log_warning(format_args!(
|
||||
@@ -271,7 +306,7 @@ where
|
||||
let refetch = ArcRwSignal::new(0);
|
||||
let source = ArcMemo::new({
|
||||
let refetch = refetch.clone();
|
||||
move |_| (refetch.get(), source())
|
||||
move |_| (refetch.get(), run_in_resource_source_signal(&source))
|
||||
});
|
||||
let fun = {
|
||||
let source = source.clone();
|
||||
@@ -909,7 +944,9 @@ where
|
||||
computed::suspense::SuspenseContext, effect::in_effect_scope,
|
||||
owner::use_context,
|
||||
};
|
||||
if !in_effect_scope() && use_context::<SuspenseContext>().is_none()
|
||||
if !in_effect_scope()
|
||||
&& !in_resource_source_signal()
|
||||
&& use_context::<SuspenseContext>().is_none()
|
||||
{
|
||||
let location = std::panic::Location::caller();
|
||||
reactive_graph::log_warning(format_args!(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "reactive_graph"
|
||||
version = "0.2.9"
|
||||
version = "0.2.12"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -29,9 +29,10 @@ send_wrapper = { features = [
|
||||
], workspace = true, default-features = true }
|
||||
subsecond = { workspace = true, default-features = true, optional = true }
|
||||
indexmap = { workspace = true, default-features = true }
|
||||
paste = { workspace = true, default-features = true }
|
||||
|
||||
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
|
||||
web-sys = { version = "0.3.77", features = ["console"] }
|
||||
web-sys = { version = "0.3.83", features = ["console"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { features = [
|
||||
@@ -40,6 +41,7 @@ tokio = { features = [
|
||||
], workspace = true, default-features = true }
|
||||
tokio-test = { workspace = true, default-features = true }
|
||||
any_spawner = { workspace = true, features = ["futures-executor", "tokio"] }
|
||||
typed-builder.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
rustc_version = { workspace = true, default-features = true }
|
||||
|
||||
@@ -2,48 +2,19 @@
|
||||
//! for component properties, because they can be used to define optional callback functions,
|
||||
//! which generic props don’t support.
|
||||
//!
|
||||
//! # Usage
|
||||
//! Callbacks can be created manually from any function or closure, but the easiest way
|
||||
//! to create them is to use `#[prop(into)]]` when defining a component.
|
||||
//! ```
|
||||
//! use leptos::prelude::*;
|
||||
//!
|
||||
//! #[component]
|
||||
//! fn MyComponent(
|
||||
//! #[prop(into)] render_number: Callback<(i32,), String>,
|
||||
//! ) -> impl IntoView {
|
||||
//! view! {
|
||||
//! <div>
|
||||
//! {render_number.run((1,))}
|
||||
//! // callbacks can be called multiple times
|
||||
//! {render_number.run((42,))}
|
||||
//! </div>
|
||||
//! }
|
||||
//! }
|
||||
//! // you can pass a closure directly as `render_number`
|
||||
//! fn test() -> impl IntoView {
|
||||
//! view! {
|
||||
//! <MyComponent render_number=|x: i32| x.to_string()/>
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! *Notes*:
|
||||
//! - The `render_number` prop can receive any type that implements `Fn(i32) -> String`.
|
||||
//! - Callbacks are most useful when you want optional generic props.
|
||||
//! - All callbacks implement the [`Callable`](leptos::callback::Callable) trait, and can be invoked with `my_callback.run(input)`.
|
||||
//! - The callback types implement [`Copy`], so they can easily be moved into and out of other closures, just like signals.
|
||||
//! The callback types implement [`Copy`], so they can easily be moved into and out of other closures, just like signals.
|
||||
//!
|
||||
//! # Types
|
||||
//! This modules implements 2 callback types:
|
||||
//! - [`Callback`](leptos::callback::Callback)
|
||||
//! - [`UnsyncCallback`](leptos::callback::UnsyncCallback)
|
||||
//! - [`Callback`](reactive_graph::callback::Callback)
|
||||
//! - [`UnsyncCallback`](reactive_graph::callback::UnsyncCallback)
|
||||
//!
|
||||
//! Use `SyncCallback` if the function is not `Sync` and `Send`.
|
||||
|
||||
use reactive_graph::{
|
||||
use crate::{
|
||||
owner::{LocalStorage, StoredValue},
|
||||
traits::{Dispose, WithValue},
|
||||
IntoReactiveValue,
|
||||
};
|
||||
use std::{fmt, rc::Rc, sync::Arc};
|
||||
|
||||
@@ -60,7 +31,16 @@ pub trait Callable<In: 'static, Out: 'static = ()> {
|
||||
fn run(&self, input: In) -> Out;
|
||||
}
|
||||
|
||||
/// A callback type that is not required to be `Send + Sync`.
|
||||
/// A callback type that is not required to be [`Send`] or [`Sync`].
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use reactive_graph::prelude::*; use reactive_graph::callback::*; let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
/// let _: UnsyncCallback<()> = UnsyncCallback::new(|_| {});
|
||||
/// let _: UnsyncCallback<(i32, i32)> = (|_x: i32, _y: i32| {}).into();
|
||||
/// let cb: UnsyncCallback<i32, String> = UnsyncCallback::new(|x: i32| x.to_string());
|
||||
/// assert_eq!(cb.run(42), "42".to_string());
|
||||
/// ```
|
||||
pub struct UnsyncCallback<In: 'static, Out: 'static = ()>(
|
||||
StoredValue<Rc<dyn Fn(In) -> Out>, LocalStorage>,
|
||||
);
|
||||
@@ -148,28 +128,15 @@ impl_unsync_callable_from_fn!(
|
||||
P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12
|
||||
);
|
||||
|
||||
/// Callbacks define a standard way to store functions and closures.
|
||||
/// A callback type that is [`Send`] + [`Sync`].
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use leptos::prelude::*;
|
||||
/// # use leptos::callback::{Callable, Callback};
|
||||
/// #[component]
|
||||
/// fn MyComponent(
|
||||
/// #[prop(into)] render_number: Callback<(i32,), String>,
|
||||
/// ) -> impl IntoView {
|
||||
/// view! {
|
||||
/// <div>
|
||||
/// {render_number.run((42,))}
|
||||
/// </div>
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// fn test() -> impl IntoView {
|
||||
/// view! {
|
||||
/// <MyComponent render_number=move |x: i32| x.to_string()/>
|
||||
/// }
|
||||
/// }
|
||||
/// # use reactive_graph::prelude::*; use reactive_graph::callback::*; let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
/// let _: Callback<()> = Callback::new(|_| {});
|
||||
/// let _: Callback<(i32, i32)> = (|_x: i32, _y: i32| {}).into();
|
||||
/// let cb: Callback<i32, String> = Callback::new(|x: i32| x.to_string());
|
||||
/// assert_eq!(cb.run(42), "42".to_string());
|
||||
/// ```
|
||||
pub struct Callback<In, Out = ()>(
|
||||
StoredValue<Arc<dyn Fn(In) -> Out + Send + Sync>>,
|
||||
@@ -241,6 +208,7 @@ impl_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12);
|
||||
|
||||
impl<In: 'static, Out: 'static> Callback<In, Out> {
|
||||
/// Creates a new callback from the given function.
|
||||
#[track_caller]
|
||||
pub fn new<F>(fun: F) -> Self
|
||||
where
|
||||
F: Fn(In) -> Out + Send + Sync + 'static,
|
||||
@@ -262,22 +230,94 @@ impl<In: 'static, Out: 'static> Callback<In, Out> {
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub struct __IntoReactiveValueMarkerCallbackSingleParam;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub struct __IntoReactiveValueMarkerCallbackStrOutputToString;
|
||||
|
||||
impl<I, O, F>
|
||||
IntoReactiveValue<
|
||||
Callback<I, O>,
|
||||
__IntoReactiveValueMarkerCallbackSingleParam,
|
||||
> for F
|
||||
where
|
||||
F: Fn(I) -> O + Send + Sync + 'static,
|
||||
{
|
||||
#[track_caller]
|
||||
fn into_reactive_value(self) -> Callback<I, O> {
|
||||
Callback::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<I, O, F>
|
||||
IntoReactiveValue<
|
||||
UnsyncCallback<I, O>,
|
||||
__IntoReactiveValueMarkerCallbackSingleParam,
|
||||
> for F
|
||||
where
|
||||
F: Fn(I) -> O + 'static,
|
||||
{
|
||||
#[track_caller]
|
||||
fn into_reactive_value(self) -> UnsyncCallback<I, O> {
|
||||
UnsyncCallback::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<I, F>
|
||||
IntoReactiveValue<
|
||||
Callback<I, String>,
|
||||
__IntoReactiveValueMarkerCallbackStrOutputToString,
|
||||
> for F
|
||||
where
|
||||
F: Fn(I) -> &'static str + Send + Sync + 'static,
|
||||
{
|
||||
#[track_caller]
|
||||
fn into_reactive_value(self) -> Callback<I, String> {
|
||||
Callback::new(move |i| self(i).to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<I, F>
|
||||
IntoReactiveValue<
|
||||
UnsyncCallback<I, String>,
|
||||
__IntoReactiveValueMarkerCallbackStrOutputToString,
|
||||
> for F
|
||||
where
|
||||
F: Fn(I) -> &'static str + 'static,
|
||||
{
|
||||
#[track_caller]
|
||||
fn into_reactive_value(self) -> UnsyncCallback<I, String> {
|
||||
UnsyncCallback::new(move |i| self(i).to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Callable;
|
||||
use crate::callback::{Callback, UnsyncCallback};
|
||||
use reactive_graph::traits::Dispose;
|
||||
use crate::{
|
||||
callback::{Callback, UnsyncCallback},
|
||||
owner::Owner,
|
||||
traits::Dispose,
|
||||
IntoReactiveValue,
|
||||
};
|
||||
|
||||
struct NoClone {}
|
||||
|
||||
#[test]
|
||||
fn clone_callback() {
|
||||
let owner = Owner::new();
|
||||
owner.set();
|
||||
|
||||
let callback = Callback::new(move |_no_clone: NoClone| NoClone {});
|
||||
let _cloned = callback;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clone_unsync_callback() {
|
||||
let owner = Owner::new();
|
||||
owner.set();
|
||||
|
||||
let callback =
|
||||
UnsyncCallback::new(move |_no_clone: NoClone| NoClone {});
|
||||
let _cloned = callback;
|
||||
@@ -285,20 +325,39 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn runback_from() {
|
||||
let owner = Owner::new();
|
||||
owner.set();
|
||||
|
||||
let _callback: Callback<(), String> = (|| "test").into();
|
||||
let _callback: Callback<(i32, String), String> =
|
||||
(|num, s| format!("{num} {s}")).into();
|
||||
// Single params should work without needing the (foo,) tuple using IntoReactiveValue:
|
||||
let _callback: Callback<usize, &'static str> =
|
||||
(|_usize| "test").into_reactive_value();
|
||||
let _callback: Callback<usize, String> =
|
||||
(|_usize| "test").into_reactive_value();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_callback_from() {
|
||||
let owner = Owner::new();
|
||||
owner.set();
|
||||
|
||||
let _callback: UnsyncCallback<(), String> = (|| "test").into();
|
||||
let _callback: UnsyncCallback<(i32, String), String> =
|
||||
(|num, s| format!("{num} {s}")).into();
|
||||
// Single params should work without needing the (foo,) tuple using IntoReactiveValue:
|
||||
let _callback: UnsyncCallback<usize, &'static str> =
|
||||
(|_usize| "test").into_reactive_value();
|
||||
let _callback: UnsyncCallback<usize, String> =
|
||||
(|_usize| "test").into_reactive_value();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_callback_try_run() {
|
||||
let owner = Owner::new();
|
||||
owner.set();
|
||||
|
||||
let callback = Callback::new(move |arg| arg);
|
||||
assert_eq!(callback.try_run((0,)), Some((0,)));
|
||||
callback.dispose();
|
||||
@@ -307,6 +366,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn unsync_callback_try_run() {
|
||||
let owner = Owner::new();
|
||||
owner.set();
|
||||
|
||||
let callback = UnsyncCallback::new(move |arg| arg);
|
||||
assert_eq!(callback.try_run((0,)), Some((0,)));
|
||||
callback.dispose();
|
||||
@@ -315,6 +377,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn callback_matches_same() {
|
||||
let owner = Owner::new();
|
||||
owner.set();
|
||||
|
||||
let callback1 = Callback::new(|x: i32| x * 2);
|
||||
let callback2 = callback1;
|
||||
assert!(callback1.matches(&callback2));
|
||||
@@ -322,6 +387,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn callback_matches_different() {
|
||||
let owner = Owner::new();
|
||||
owner.set();
|
||||
|
||||
let callback1 = Callback::new(|x: i32| x * 2);
|
||||
let callback2 = Callback::new(|x: i32| x + 1);
|
||||
assert!(!callback1.matches(&callback2));
|
||||
@@ -329,6 +397,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn unsync_callback_matches_same() {
|
||||
let owner = Owner::new();
|
||||
owner.set();
|
||||
|
||||
let callback1 = UnsyncCallback::new(|x: i32| x * 2);
|
||||
let callback2 = callback1;
|
||||
assert!(callback1.matches(&callback2));
|
||||
@@ -336,6 +407,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn unsync_callback_matches_different() {
|
||||
let owner = Owner::new();
|
||||
owner.set();
|
||||
|
||||
let callback1 = UnsyncCallback::new(|x: i32| x * 2);
|
||||
let callback2 = UnsyncCallback::new(|x: i32| x + 1);
|
||||
assert!(!callback1.matches(&callback2));
|
||||
@@ -632,12 +632,29 @@ impl<T: 'static> ReadUntracked for ArcAsyncDerived<T> {
|
||||
|
||||
fn try_read_untracked(&self) -> Option<Self::Value> {
|
||||
if let Some(suspense_context) = use_context::<SuspenseContext>() {
|
||||
// create a handle to register it with suspense
|
||||
let handle = suspense_context.task_id();
|
||||
let ready = SpecialNonReactiveFuture::new(self.ready());
|
||||
crate::spawn(async move {
|
||||
ready.await;
|
||||
drop(handle);
|
||||
});
|
||||
|
||||
// check if the task is *already* ready
|
||||
let mut ready =
|
||||
Box::pin(SpecialNonReactiveFuture::new(self.ready()));
|
||||
match ready.as_mut().now_or_never() {
|
||||
Some(_) => {
|
||||
// if it's already ready, drop the handle immediately
|
||||
// this will immediately notify the suspense context that it's complete
|
||||
drop(handle);
|
||||
}
|
||||
None => {
|
||||
// otherwise, spawn a task to wait for it to be ready, then drop the handle,
|
||||
// which will notify the suspense
|
||||
crate::spawn(async move {
|
||||
ready.await;
|
||||
drop(handle);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// register the suspense context with our list of them, to be notified later if this re-runs
|
||||
self.inner
|
||||
.write()
|
||||
.or_poisoned()
|
||||
|
||||
@@ -110,10 +110,12 @@ fn effect_base() -> (Receiver, Owner, Arc<RwLock<EffectInner>>) {
|
||||
(rx, owner, inner)
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
thread_local! {
|
||||
static EFFECT_SCOPE_ACTIVE: AtomicBool = const { AtomicBool::new(false) };
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
/// Returns whether the current thread is currently running an effect.
|
||||
pub fn in_effect_scope() -> bool {
|
||||
EFFECT_SCOPE_ACTIVE
|
||||
@@ -123,14 +125,22 @@ pub fn in_effect_scope() -> bool {
|
||||
/// Set a static to true whilst running the given function.
|
||||
/// [`is_in_effect_scope`] will return true whilst the function is running.
|
||||
fn run_in_effect_scope<T>(fun: impl FnOnce() -> T) -> T {
|
||||
// For the theoretical nested case, set back to initial value rather than false:
|
||||
let initial = EFFECT_SCOPE_ACTIVE
|
||||
.with(|scope| scope.swap(true, std::sync::atomic::Ordering::Relaxed));
|
||||
let result = fun();
|
||||
EFFECT_SCOPE_ACTIVE.with(|scope| {
|
||||
scope.store(initial, std::sync::atomic::Ordering::Relaxed)
|
||||
});
|
||||
result
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
// For the theoretical nested case, set back to initial value rather than false:
|
||||
let initial = EFFECT_SCOPE_ACTIVE.with(|scope| {
|
||||
scope.swap(true, std::sync::atomic::Ordering::Relaxed)
|
||||
});
|
||||
let result = fun();
|
||||
EFFECT_SCOPE_ACTIVE.with(|scope| {
|
||||
scope.store(initial, std::sync::atomic::Ordering::Relaxed)
|
||||
});
|
||||
result
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
fun()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Effect<S>
|
||||
|
||||
67
reactive_graph/src/into_reactive_value.rs
Normal file
67
reactive_graph/src/into_reactive_value.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
#[doc(hidden)]
|
||||
pub struct __IntoReactiveValueMarkerBaseCase;
|
||||
|
||||
/// A helper trait that works like `Into<T>` but uses a marker generic
|
||||
/// to allow more `From` implementations than would be allowed with just `Into<T>`.
|
||||
pub trait IntoReactiveValue<T, M> {
|
||||
/// Converts `self` into a `T`.
|
||||
fn into_reactive_value(self) -> T;
|
||||
}
|
||||
|
||||
// The base case, which allows anything which implements .into() to work:
|
||||
impl<T, I> IntoReactiveValue<T, __IntoReactiveValueMarkerBaseCase> for I
|
||||
where
|
||||
I: Into<T>,
|
||||
{
|
||||
fn into_reactive_value(self) -> T {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use crate::{
|
||||
into_reactive_value::IntoReactiveValue,
|
||||
owner::{LocalStorage, Owner},
|
||||
traits::GetUntracked,
|
||||
wrappers::read::Signal,
|
||||
};
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
#[test]
|
||||
fn test_into_signal_compiles() {
|
||||
let owner = Owner::new();
|
||||
owner.set();
|
||||
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
let _: Signal<usize> = (|| 2).into_reactive_value();
|
||||
let _: Signal<usize, LocalStorage> = 2.into_reactive_value();
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
let _: Signal<usize, LocalStorage> = (|| 2).into_reactive_value();
|
||||
let _: Signal<String> = "str".into_reactive_value();
|
||||
let _: Signal<String, LocalStorage> = "str".into_reactive_value();
|
||||
|
||||
#[derive(TypedBuilder)]
|
||||
struct Foo {
|
||||
#[builder(setter(
|
||||
fn transform<M>(value: impl IntoReactiveValue<Signal<usize>, M>) {
|
||||
value.into_reactive_value()
|
||||
}
|
||||
))]
|
||||
sig: Signal<usize>,
|
||||
}
|
||||
|
||||
assert_eq!(Foo::builder().sig(2).build().sig.get_untracked(), 2);
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
assert_eq!(Foo::builder().sig(|| 2).build().sig.get_untracked(), 2);
|
||||
assert_eq!(
|
||||
Foo::builder()
|
||||
.sig(Signal::stored(2))
|
||||
.build()
|
||||
.sig
|
||||
.get_untracked(),
|
||||
2
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -90,6 +90,12 @@ pub mod traits;
|
||||
pub mod transition;
|
||||
pub mod wrappers;
|
||||
|
||||
mod into_reactive_value;
|
||||
pub use into_reactive_value::*;
|
||||
|
||||
/// A standard way to wrap functions and closures to pass them to components.
|
||||
pub mod callback;
|
||||
|
||||
use computed::ScopedFuture;
|
||||
|
||||
#[cfg(all(feature = "nightly", rustc_nightly))]
|
||||
@@ -97,7 +103,9 @@ mod nightly;
|
||||
|
||||
/// Reexports frequently-used traits.
|
||||
pub mod prelude {
|
||||
pub use crate::{owner::FromLocal, traits::*};
|
||||
pub use crate::{
|
||||
into_reactive_value::IntoReactiveValue, owner::FromLocal, traits::*,
|
||||
};
|
||||
}
|
||||
|
||||
// TODO remove this, it's just useful while developing
|
||||
|
||||
@@ -340,6 +340,8 @@ impl Owner {
|
||||
}
|
||||
|
||||
/// Removes this from its state as the thread-local owner and drops it.
|
||||
/// If there are other holders of this owner, it may not cleanup, if always cleaning up is required,
|
||||
/// see [`Owner::unset_with_forced_cleanup`].
|
||||
pub fn unset(self) {
|
||||
OWNER.with_borrow_mut(|owner| {
|
||||
if owner.as_ref().and_then(|n| n.upgrade()) == Some(self) {
|
||||
@@ -348,6 +350,23 @@ impl Owner {
|
||||
})
|
||||
}
|
||||
|
||||
/// Removes this from its state as the thread-local owner and drops it.
|
||||
/// Unlike [`Owner::unset`], this will always run cleanup on this owner,
|
||||
/// even if there are other holders of this owner.
|
||||
pub fn unset_with_forced_cleanup(self) {
|
||||
OWNER.with_borrow_mut(|owner| {
|
||||
if owner
|
||||
.as_ref()
|
||||
.and_then(|n| n.upgrade())
|
||||
.map(|o| o == self)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
mem::take(owner);
|
||||
}
|
||||
});
|
||||
self.cleanup();
|
||||
}
|
||||
|
||||
/// Returns the current [`SharedContext`], if any.
|
||||
#[cfg(feature = "hydration")]
|
||||
pub fn current_shared_context(
|
||||
|
||||
@@ -324,6 +324,22 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> From<&'static str> for ArcSignal<String, S>
|
||||
where
|
||||
S: Storage<&'static str> + Storage<String>,
|
||||
{
|
||||
#[track_caller]
|
||||
fn from(value: &'static str) -> Self {
|
||||
Self {
|
||||
inner: SignalTypes::Stored(ArcStoredValue::new(
|
||||
value.to_string(),
|
||||
)),
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: std::panic::Location::caller(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> DefinedAt for ArcSignal<T, S>
|
||||
where
|
||||
S: Storage<T>,
|
||||
@@ -1049,6 +1065,13 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Signal<&'static str, LocalStorage>> for Signal<String, LocalStorage> {
|
||||
#[track_caller]
|
||||
fn from(value: Signal<&'static str, LocalStorage>) -> Self {
|
||||
Signal::derive_local(move || value.read().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Signal<&'static str>> for Signal<String, LocalStorage> {
|
||||
#[track_caller]
|
||||
fn from(value: Signal<&'static str>) -> Self {
|
||||
@@ -1077,6 +1100,15 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Signal<Option<&'static str>, LocalStorage>>
|
||||
for Signal<Option<String>, LocalStorage>
|
||||
{
|
||||
#[track_caller]
|
||||
fn from(value: Signal<Option<&'static str>, LocalStorage>) -> Self {
|
||||
Signal::derive_local(move || value.read().map(str::to_string))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Signal<Option<&'static str>>>
|
||||
for Signal<Option<String>, LocalStorage>
|
||||
{
|
||||
@@ -1086,6 +1118,192 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
#[doc(hidden)]
|
||||
pub struct __IntoReactiveValueMarkerSignalFromReactiveClosure;
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
#[doc(hidden)]
|
||||
pub struct __IntoReactiveValueMarkerSignalStrOutputToString;
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
#[doc(hidden)]
|
||||
pub struct __IntoReactiveValueMarkerOptionalSignalFromReactiveClosureAlways;
|
||||
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
impl<T, F>
|
||||
crate::IntoReactiveValue<
|
||||
Signal<T, SyncStorage>,
|
||||
__IntoReactiveValueMarkerSignalFromReactiveClosure,
|
||||
> for F
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
F: Fn() -> T + Send + Sync + 'static,
|
||||
{
|
||||
fn into_reactive_value(self) -> Signal<T, SyncStorage> {
|
||||
Signal::derive(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
impl<T, F>
|
||||
crate::IntoReactiveValue<
|
||||
ArcSignal<T, SyncStorage>,
|
||||
__IntoReactiveValueMarkerSignalFromReactiveClosure,
|
||||
> for F
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
F: Fn() -> T + Send + Sync + 'static,
|
||||
{
|
||||
fn into_reactive_value(self) -> ArcSignal<T, SyncStorage> {
|
||||
ArcSignal::derive(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
impl<T, F>
|
||||
crate::IntoReactiveValue<
|
||||
Signal<T, LocalStorage>,
|
||||
__IntoReactiveValueMarkerSignalFromReactiveClosure,
|
||||
> for F
|
||||
where
|
||||
T: 'static,
|
||||
F: Fn() -> T + 'static,
|
||||
{
|
||||
fn into_reactive_value(self) -> Signal<T, LocalStorage> {
|
||||
Signal::derive_local(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
impl<T, F>
|
||||
crate::IntoReactiveValue<
|
||||
ArcSignal<T, LocalStorage>,
|
||||
__IntoReactiveValueMarkerSignalFromReactiveClosure,
|
||||
> for F
|
||||
where
|
||||
T: 'static,
|
||||
F: Fn() -> T + 'static,
|
||||
{
|
||||
fn into_reactive_value(self) -> ArcSignal<T, LocalStorage> {
|
||||
ArcSignal::derive_local(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
impl<F>
|
||||
crate::IntoReactiveValue<
|
||||
Signal<String, SyncStorage>,
|
||||
__IntoReactiveValueMarkerSignalStrOutputToString,
|
||||
> for F
|
||||
where
|
||||
F: Fn() -> &'static str + Send + Sync + 'static,
|
||||
{
|
||||
fn into_reactive_value(self) -> Signal<String, SyncStorage> {
|
||||
Signal::derive(move || self().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
impl<F>
|
||||
crate::IntoReactiveValue<
|
||||
ArcSignal<String, SyncStorage>,
|
||||
__IntoReactiveValueMarkerSignalStrOutputToString,
|
||||
> for F
|
||||
where
|
||||
F: Fn() -> &'static str + Send + Sync + 'static,
|
||||
{
|
||||
fn into_reactive_value(self) -> ArcSignal<String, SyncStorage> {
|
||||
ArcSignal::derive(move || self().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
impl<F>
|
||||
crate::IntoReactiveValue<
|
||||
Signal<String, LocalStorage>,
|
||||
__IntoReactiveValueMarkerSignalStrOutputToString,
|
||||
> for F
|
||||
where
|
||||
F: Fn() -> &'static str + 'static,
|
||||
{
|
||||
fn into_reactive_value(self) -> Signal<String, LocalStorage> {
|
||||
Signal::derive_local(move || self().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
impl<F>
|
||||
crate::IntoReactiveValue<
|
||||
ArcSignal<String, LocalStorage>,
|
||||
__IntoReactiveValueMarkerSignalStrOutputToString,
|
||||
> for F
|
||||
where
|
||||
F: Fn() -> &'static str + 'static,
|
||||
{
|
||||
fn into_reactive_value(self) -> ArcSignal<String, LocalStorage> {
|
||||
ArcSignal::derive_local(move || self().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
impl<T, F>
|
||||
crate::IntoReactiveValue<
|
||||
Signal<Option<T>, SyncStorage>,
|
||||
__IntoReactiveValueMarkerOptionalSignalFromReactiveClosureAlways,
|
||||
> for F
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
F: Fn() -> T + Send + Sync + 'static,
|
||||
{
|
||||
fn into_reactive_value(self) -> Signal<Option<T>, SyncStorage> {
|
||||
Signal::derive(move || Some(self()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
impl<T, F>
|
||||
crate::IntoReactiveValue<
|
||||
ArcSignal<Option<T>, SyncStorage>,
|
||||
__IntoReactiveValueMarkerOptionalSignalFromReactiveClosureAlways,
|
||||
> for F
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
F: Fn() -> T + Send + Sync + 'static,
|
||||
{
|
||||
fn into_reactive_value(self) -> ArcSignal<Option<T>, SyncStorage> {
|
||||
ArcSignal::derive(move || Some(self()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
impl<T, F>
|
||||
crate::IntoReactiveValue<
|
||||
Signal<Option<T>, LocalStorage>,
|
||||
__IntoReactiveValueMarkerOptionalSignalFromReactiveClosureAlways,
|
||||
> for F
|
||||
where
|
||||
T: 'static,
|
||||
F: Fn() -> T + 'static,
|
||||
{
|
||||
fn into_reactive_value(self) -> Signal<Option<T>, LocalStorage> {
|
||||
Signal::derive_local(move || Some(self()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
impl<T, F>
|
||||
crate::IntoReactiveValue<
|
||||
ArcSignal<Option<T>, LocalStorage>,
|
||||
__IntoReactiveValueMarkerOptionalSignalFromReactiveClosureAlways,
|
||||
> for F
|
||||
where
|
||||
T: 'static,
|
||||
F: Fn() -> T + 'static,
|
||||
{
|
||||
fn into_reactive_value(self) -> ArcSignal<Option<T>, LocalStorage> {
|
||||
ArcSignal::derive_local(move || Some(self()))
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<T> From<MaybeSignal<T>> for Signal<T>
|
||||
where
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "reactive_stores"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
|
||||
@@ -29,6 +29,7 @@ where
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: &'static Location<'static>,
|
||||
path: Arc<dyn Fn() -> StorePath + Send + Sync>,
|
||||
path_unkeyed: Arc<dyn Fn() -> StorePath + Send + Sync>,
|
||||
get_trigger: Arc<dyn Fn(StorePath) -> StoreFieldTrigger + Send + Sync>,
|
||||
get_trigger_unkeyed:
|
||||
Arc<dyn Fn(StorePath) -> StoreFieldTrigger + Send + Sync>,
|
||||
@@ -113,6 +114,10 @@ impl<T> StoreField for ArcField<T> {
|
||||
(self.path)()
|
||||
}
|
||||
|
||||
fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
|
||||
(self.path_unkeyed)()
|
||||
}
|
||||
|
||||
fn reader(&self) -> Option<Self::Reader> {
|
||||
(self.read)().map(StoreFieldReader::new)
|
||||
}
|
||||
@@ -137,6 +142,9 @@ where
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: Location::caller(),
|
||||
path: Arc::new(move || value.path().into_iter().collect()),
|
||||
path_unkeyed: Arc::new(move || {
|
||||
value.path_unkeyed().into_iter().collect()
|
||||
}),
|
||||
get_trigger: Arc::new(move |path| value.get_trigger(path)),
|
||||
get_trigger_unkeyed: Arc::new(move |path| {
|
||||
value.get_trigger_unkeyed(path)
|
||||
@@ -163,6 +171,10 @@ where
|
||||
let value = value.clone();
|
||||
move || value.path().into_iter().collect()
|
||||
}),
|
||||
path_unkeyed: Arc::new({
|
||||
let value = value.clone();
|
||||
move || value.path_unkeyed().into_iter().collect()
|
||||
}),
|
||||
get_trigger: Arc::new({
|
||||
let value = value.clone();
|
||||
move |path| value.get_trigger(path)
|
||||
@@ -211,6 +223,10 @@ where
|
||||
let value = value.clone();
|
||||
move || value.path().into_iter().collect()
|
||||
}),
|
||||
path_unkeyed: Arc::new({
|
||||
let value = value.clone();
|
||||
move || value.path_unkeyed().into_iter().collect()
|
||||
}),
|
||||
get_trigger: Arc::new({
|
||||
let value = value.clone();
|
||||
move |path| value.get_trigger(path)
|
||||
@@ -258,6 +274,10 @@ where
|
||||
let value = value.clone();
|
||||
move || value.path().into_iter().collect()
|
||||
}),
|
||||
path_unkeyed: Arc::new({
|
||||
let value = value.clone();
|
||||
move || value.path_unkeyed().into_iter().collect()
|
||||
}),
|
||||
get_trigger: Arc::new({
|
||||
let value = value.clone();
|
||||
move |path| value.get_trigger(path)
|
||||
@@ -306,6 +326,10 @@ where
|
||||
let value = value.clone();
|
||||
move || value.path().into_iter().collect()
|
||||
}),
|
||||
path_unkeyed: Arc::new({
|
||||
let value = value.clone();
|
||||
move || value.path_unkeyed().into_iter().collect()
|
||||
}),
|
||||
get_trigger: Arc::new({
|
||||
let value = value.clone();
|
||||
move |path| value.get_trigger(path)
|
||||
@@ -358,6 +382,10 @@ where
|
||||
let value = value.clone();
|
||||
move || value.path().into_iter().collect()
|
||||
}),
|
||||
path_unkeyed: Arc::new({
|
||||
let value = value.clone();
|
||||
move || value.path_unkeyed().into_iter().collect()
|
||||
}),
|
||||
get_trigger: Arc::new({
|
||||
let value = value.clone();
|
||||
move |path| value.get_trigger(path)
|
||||
@@ -396,6 +424,7 @@ impl<T> Clone for ArcField<T> {
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: self.defined_at,
|
||||
path: self.path.clone(),
|
||||
path_unkeyed: self.path_unkeyed.clone(),
|
||||
get_trigger: Arc::clone(&self.get_trigger),
|
||||
get_trigger_unkeyed: Arc::clone(&self.get_trigger_unkeyed),
|
||||
read: Arc::clone(&self.read),
|
||||
|
||||
@@ -76,6 +76,11 @@ where
|
||||
fn path(&self) -> impl IntoIterator<Item = StorePathSegment> {
|
||||
self.inner.path()
|
||||
}
|
||||
|
||||
fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
|
||||
self.inner.path_unkeyed()
|
||||
}
|
||||
|
||||
fn reader(&self) -> Option<Self::Reader> {
|
||||
let inner = self.inner.reader()?;
|
||||
Some(Mapped::new_with_guard(inner, |n| n.deref()))
|
||||
|
||||
@@ -73,6 +73,13 @@ where
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
|
||||
self.inner
|
||||
.try_get_value()
|
||||
.map(|inner| inner.path_unkeyed().into_iter().collect::<Vec<_>>())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn reader(&self) -> Option<Self::Reader> {
|
||||
self.inner.try_get_value().and_then(|inner| inner.reader())
|
||||
}
|
||||
|
||||
@@ -80,6 +80,13 @@ where
|
||||
.chain(iter::once(self.index.into()))
|
||||
}
|
||||
|
||||
fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
|
||||
self.inner
|
||||
.path_unkeyed()
|
||||
.into_iter()
|
||||
.chain(iter::once(self.index.into()))
|
||||
}
|
||||
|
||||
fn get_trigger(&self, path: StorePath) -> StoreFieldTrigger {
|
||||
self.inner.get_trigger(path)
|
||||
}
|
||||
|
||||
@@ -106,6 +106,13 @@ where
|
||||
.chain(iter::once(self.path_segment))
|
||||
}
|
||||
|
||||
fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
|
||||
self.inner
|
||||
.path_unkeyed()
|
||||
.into_iter()
|
||||
.chain(iter::once(self.path_segment))
|
||||
}
|
||||
|
||||
fn get_trigger(&self, path: StorePath) -> StoreFieldTrigger {
|
||||
self.inner.get_trigger(path)
|
||||
}
|
||||
@@ -444,6 +451,24 @@ where
|
||||
inner.into_iter().chain(this)
|
||||
}
|
||||
|
||||
fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
|
||||
let inner =
|
||||
self.inner.path_unkeyed().into_iter().collect::<StorePath>();
|
||||
let keys = self
|
||||
.inner
|
||||
.keys()
|
||||
.expect("using keys on a store with no keys");
|
||||
let this = keys
|
||||
.with_field_keys(
|
||||
inner.clone(),
|
||||
|keys| (keys.get(&self.key), vec![]),
|
||||
|| self.inner.latest_keys(),
|
||||
)
|
||||
.flatten()
|
||||
.map(|(_, idx)| StorePathSegment(idx));
|
||||
inner.into_iter().chain(this)
|
||||
}
|
||||
|
||||
fn get_trigger(&self, path: StorePath) -> StoreFieldTrigger {
|
||||
self.inner.get_trigger(path)
|
||||
}
|
||||
@@ -713,3 +738,145 @@ where
|
||||
.map(|key| AtKeyed::new(self.inner.clone(), key))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{self as reactive_stores, tests::tick, AtKeyed, Store};
|
||||
use reactive_graph::{
|
||||
effect::Effect,
|
||||
traits::{GetUntracked, ReadUntracked, Set, Track, Write},
|
||||
};
|
||||
use reactive_stores::Patch;
|
||||
use std::sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc,
|
||||
};
|
||||
|
||||
#[derive(Debug, Store, Default, Patch)]
|
||||
struct Todos {
|
||||
#[store(key: usize = |todo| todo.id)]
|
||||
todos: Vec<Todo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Store, Default, Clone, PartialEq, Eq, Patch)]
|
||||
struct Todo {
|
||||
id: usize,
|
||||
label: String,
|
||||
}
|
||||
|
||||
impl Todo {
|
||||
pub fn new(id: usize, label: impl ToString) -> Self {
|
||||
Self {
|
||||
id,
|
||||
label: label.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn data() -> Todos {
|
||||
Todos {
|
||||
todos: vec![
|
||||
Todo {
|
||||
id: 10,
|
||||
label: "A".to_string(),
|
||||
},
|
||||
Todo {
|
||||
id: 11,
|
||||
label: "B".to_string(),
|
||||
},
|
||||
Todo {
|
||||
id: 12,
|
||||
label: "C".to_string(),
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn keyed_fields_can_be_moved() {
|
||||
_ = any_spawner::Executor::init_tokio();
|
||||
|
||||
let store = Store::new(data());
|
||||
assert_eq!(store.read_untracked().todos.len(), 3);
|
||||
|
||||
// create an effect to read from each keyed field
|
||||
let a_count = Arc::new(AtomicUsize::new(0));
|
||||
let b_count = Arc::new(AtomicUsize::new(0));
|
||||
let c_count = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let a = AtKeyed::new(store.todos(), 10);
|
||||
let b = AtKeyed::new(store.todos(), 11);
|
||||
let c = AtKeyed::new(store.todos(), 12);
|
||||
|
||||
Effect::new_sync({
|
||||
let a_count = Arc::clone(&a_count);
|
||||
move || {
|
||||
a.track();
|
||||
a_count.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
});
|
||||
Effect::new_sync({
|
||||
let b_count = Arc::clone(&b_count);
|
||||
move || {
|
||||
b.track();
|
||||
b_count.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
});
|
||||
Effect::new_sync({
|
||||
let c_count = Arc::clone(&c_count);
|
||||
move || {
|
||||
c.track();
|
||||
c_count.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
});
|
||||
|
||||
tick().await;
|
||||
assert_eq!(a_count.load(Ordering::Relaxed), 1);
|
||||
assert_eq!(b_count.load(Ordering::Relaxed), 1);
|
||||
assert_eq!(c_count.load(Ordering::Relaxed), 1);
|
||||
|
||||
// writing at a key doesn't notify siblings
|
||||
*a.label().write() = "Foo".into();
|
||||
tick().await;
|
||||
assert_eq!(a_count.load(Ordering::Relaxed), 2);
|
||||
assert_eq!(b_count.load(Ordering::Relaxed), 1);
|
||||
assert_eq!(c_count.load(Ordering::Relaxed), 1);
|
||||
|
||||
// the keys can be reorganized
|
||||
store.todos().write().swap(0, 2);
|
||||
let after = store.todos().get_untracked();
|
||||
assert_eq!(
|
||||
after,
|
||||
vec![Todo::new(12, "C"), Todo::new(11, "B"), Todo::new(10, "Foo")]
|
||||
);
|
||||
|
||||
tick().await;
|
||||
assert_eq!(a_count.load(Ordering::Relaxed), 2);
|
||||
assert_eq!(b_count.load(Ordering::Relaxed), 1);
|
||||
assert_eq!(c_count.load(Ordering::Relaxed), 1);
|
||||
|
||||
// and after we move the keys around, they still update the moved items
|
||||
a.label().set("Bar".into());
|
||||
let after = store.todos().get_untracked();
|
||||
assert_eq!(
|
||||
after,
|
||||
vec![Todo::new(12, "C"), Todo::new(11, "B"), Todo::new(10, "Bar")]
|
||||
);
|
||||
tick().await;
|
||||
assert_eq!(a_count.load(Ordering::Relaxed), 3);
|
||||
assert_eq!(b_count.load(Ordering::Relaxed), 1);
|
||||
assert_eq!(c_count.load(Ordering::Relaxed), 1);
|
||||
|
||||
// we can remove a key and add a new one
|
||||
store.todos().write().pop();
|
||||
store.todos().write().push(Todo::new(13, "New"));
|
||||
let after = store.todos().get_untracked();
|
||||
assert_eq!(
|
||||
after,
|
||||
vec![Todo::new(12, "C"), Todo::new(11, "B"), Todo::new(13, "New")]
|
||||
);
|
||||
tick().await;
|
||||
assert_eq!(a_count.load(Ordering::Relaxed), 3);
|
||||
assert_eq!(b_count.load(Ordering::Relaxed), 1);
|
||||
assert_eq!(c_count.load(Ordering::Relaxed), 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ where
|
||||
type Value = T::Value;
|
||||
|
||||
fn patch(&self, new: Self::Value) {
|
||||
let path = self.path().into_iter().collect::<StorePath>();
|
||||
let path = self.path_unkeyed().into_iter().collect::<StorePath>();
|
||||
if let Some(mut writer) = self.writer() {
|
||||
// don't track the writer for the whole store
|
||||
writer.untrack();
|
||||
|
||||
@@ -38,6 +38,13 @@ pub trait StoreField: Sized {
|
||||
#[track_caller]
|
||||
fn path(&self) -> impl IntoIterator<Item = StorePathSegment>;
|
||||
|
||||
/// The path of this field (see [`StorePath`]). Uses unkeyed indices for any keyed fields.
|
||||
#[track_caller]
|
||||
fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
|
||||
// TODO remove default impl next time we do a breaking release
|
||||
self.path()
|
||||
}
|
||||
|
||||
/// Reactively tracks this field.
|
||||
#[track_caller]
|
||||
fn track_field(&self) {
|
||||
@@ -129,7 +136,9 @@ where
|
||||
trigger
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn get_trigger_unkeyed(&self, path: StorePath) -> StoreFieldTrigger {
|
||||
let caller = std::panic::Location::caller();
|
||||
let orig_path = path.clone();
|
||||
|
||||
let mut path = StorePath::with_capacity(orig_path.len());
|
||||
@@ -140,7 +149,13 @@ where
|
||||
let key = self
|
||||
.keys
|
||||
.get_key_for_index(&(path.clone(), segment.0))
|
||||
.expect("could not find key for index");
|
||||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"could not find key for index {:?} at {}",
|
||||
&(path.clone(), segment.0),
|
||||
caller
|
||||
)
|
||||
});
|
||||
path.push(key);
|
||||
} else {
|
||||
path.push(*segment);
|
||||
@@ -154,6 +169,11 @@ where
|
||||
iter::empty()
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
|
||||
iter::empty()
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn reader(&self) -> Option<Self::Reader> {
|
||||
Plain::try_new(Arc::clone(&self.value))
|
||||
@@ -205,6 +225,14 @@ where
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
|
||||
self.inner
|
||||
.try_get_value()
|
||||
.map(|n| n.path_unkeyed().into_iter().collect::<Vec<_>>())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn reader(&self) -> Option<Self::Reader> {
|
||||
self.inner.try_get_value().and_then(|n| n.reader())
|
||||
|
||||
@@ -84,6 +84,13 @@ where
|
||||
.chain(iter::once(self.path_segment))
|
||||
}
|
||||
|
||||
fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
|
||||
self.inner
|
||||
.path_unkeyed()
|
||||
.into_iter()
|
||||
.chain(iter::once(self.path_segment))
|
||||
}
|
||||
|
||||
fn get_trigger(&self, path: StorePath) -> StoreFieldTrigger {
|
||||
self.inner.get_trigger(path)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_router"
|
||||
version = "0.8.9"
|
||||
version = "0.8.11"
|
||||
authors = ["Greg Johnston", "Ben Wishovich"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
//! move || params.read().get("id").unwrap_or_default(),
|
||||
//! move |id| contact_data(id)
|
||||
//! );
|
||||
//! todo!()
|
||||
//! // ... return some view
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
|
||||
@@ -368,8 +368,8 @@ where
|
||||
ev.prevent_default();
|
||||
let to = path_name
|
||||
+ if url.search.is_empty() { "" } else { "?" }
|
||||
+ &Url::unescape(&url.search)
|
||||
+ &Url::unescape(&url.hash);
|
||||
+ &url.search
|
||||
+ &url.hash;
|
||||
let state = Reflect::get(&a, &JsValue::from_str("state"))
|
||||
.ok()
|
||||
.and_then(|value| {
|
||||
|
||||
@@ -4,7 +4,6 @@ macro_rules! tuples {
|
||||
($first:ident => $($ty:ident),*) => {
|
||||
impl<$first, $($ty),*> PossibleRouteMatch for ($first, $($ty,)*)
|
||||
where
|
||||
Self: core::fmt::Debug,
|
||||
$first: PossibleRouteMatch,
|
||||
$($ty: PossibleRouteMatch),*,
|
||||
{
|
||||
|
||||
@@ -369,7 +369,7 @@ impl ResolvedStaticPath {
|
||||
eprintln!("{e}");
|
||||
}
|
||||
}
|
||||
owner.unset();
|
||||
owner.unset_with_forced_cleanup();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_router_macro"
|
||||
version = "0.8.5"
|
||||
version = "0.8.6"
|
||||
authors = ["Greg Johnston", "Ben Wishovich"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
use proc_macro::{TokenStream, TokenTree};
|
||||
use proc_macro2::Span;
|
||||
use proc_macro_error2::{abort, proc_macro_error, set_dummy};
|
||||
use quote::{quote, ToTokens};
|
||||
use quote::{format_ident, quote, ToTokens};
|
||||
use syn::{
|
||||
spanned::Spanned, FnArg, Ident, ImplItem, ItemImpl, Path, Type, TypePath,
|
||||
};
|
||||
@@ -267,10 +267,7 @@ fn lazy_route_impl(
|
||||
};
|
||||
let lazy_view_ident =
|
||||
Ident::new(&format!("__{ty_name_to_snake}_View"), im.self_ty.span());
|
||||
let preload_lazy_view_ident = Ident::new(
|
||||
&format!("__preload_{lazy_view_ident}"),
|
||||
lazy_view_ident.span(),
|
||||
);
|
||||
let preload_ident = format_ident!("__preload_{lazy_view_ident}");
|
||||
|
||||
im.items.push(
|
||||
syn::parse::<ImplItem>(
|
||||
@@ -280,7 +277,7 @@ fn lazy_route_impl(
|
||||
// we don't split routes for wasm32 ssr
|
||||
// but we don't require a `hydrate`/`csr` feature on leptos_router
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#preload_lazy_view_ident().await;
|
||||
#preload_ident().await;
|
||||
}
|
||||
}
|
||||
.into(),
|
||||
|
||||
@@ -5,7 +5,7 @@ license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "RPC for any web framework."
|
||||
readme = "../README.md"
|
||||
version = "0.8.8"
|
||||
version = "0.8.9"
|
||||
rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -32,7 +32,7 @@ dashmap = { workspace = true, default-features = true }
|
||||
|
||||
## servers
|
||||
# actix
|
||||
actix-web = { optional = true, workspace = true, default-features = false }
|
||||
actix-web = { optional = true, workspace = true, default-features = false, features = ["ws"] }
|
||||
actix-ws = { optional = true, workspace = true, default-features = true }
|
||||
|
||||
# axum
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tachys"
|
||||
version = "0.2.10"
|
||||
version = "0.2.11"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
|
||||
@@ -57,6 +57,10 @@ where
|
||||
_style: &mut String,
|
||||
_inner_html: &mut String,
|
||||
) {
|
||||
// If this is a class="..." attribute (not class:name=value), clear previous value
|
||||
if self.class.should_overwrite() {
|
||||
class.clear();
|
||||
}
|
||||
class.push(' ');
|
||||
self.class.to_html(class);
|
||||
}
|
||||
@@ -156,6 +160,12 @@ pub trait IntoClass: Send {
|
||||
/// Renders the class to HTML.
|
||||
fn to_html(self, class: &mut String);
|
||||
|
||||
/// Whether this class attribute should overwrite previous class values.
|
||||
/// Returns `true` for `class="..."` attributes, `false` for `class:name=value` directives.
|
||||
fn should_overwrite(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Renders the class to HTML for a `<template>`.
|
||||
#[allow(unused)] // it's used with `nightly` feature
|
||||
fn to_template(class: &mut String) {}
|
||||
@@ -289,6 +299,10 @@ impl IntoClass for &str {
|
||||
class.push_str(self);
|
||||
}
|
||||
|
||||
fn should_overwrite(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
self,
|
||||
el: &crate::renderer::types::Element,
|
||||
@@ -346,6 +360,10 @@ impl IntoClass for Cow<'_, str> {
|
||||
IntoClass::to_html(&*self, class);
|
||||
}
|
||||
|
||||
fn should_overwrite(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
self,
|
||||
el: &crate::renderer::types::Element,
|
||||
@@ -403,6 +421,10 @@ impl IntoClass for String {
|
||||
IntoClass::to_html(self.as_str(), class);
|
||||
}
|
||||
|
||||
fn should_overwrite(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
self,
|
||||
el: &crate::renderer::types::Element,
|
||||
@@ -460,6 +482,10 @@ impl IntoClass for Arc<str> {
|
||||
IntoClass::to_html(self.as_ref(), class);
|
||||
}
|
||||
|
||||
fn should_overwrite(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
self,
|
||||
el: &crate::renderer::types::Element,
|
||||
|
||||
@@ -47,11 +47,13 @@ pub fn directive<T, P, D>(handler: D, param: P) -> Directive<T, D, P>
|
||||
where
|
||||
D: IntoDirective<T, P>,
|
||||
{
|
||||
Directive(Some(SendWrapper::new(DirectiveInner {
|
||||
handler,
|
||||
param,
|
||||
t: PhantomData,
|
||||
})))
|
||||
Directive((!cfg!(feature = "ssr")).then(|| {
|
||||
SendWrapper::new(DirectiveInner {
|
||||
handler,
|
||||
param,
|
||||
t: PhantomData,
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
/// Custom logic that runs in the browser when the element is created or hydrated.
|
||||
@@ -151,13 +153,7 @@ where
|
||||
Directive(inner)
|
||||
}
|
||||
|
||||
fn dry_resolve(&mut self) {
|
||||
// dry_resolve() only runs during SSR, and we should use it to
|
||||
// synchronously remove and drop the SendWrapper value
|
||||
// we don't need this value during SSR and leaving it here could drop it
|
||||
// from a different thread
|
||||
self.0.take();
|
||||
}
|
||||
fn dry_resolve(&mut self) {}
|
||||
|
||||
async fn resolve(self) -> Self::AsyncOutput {
|
||||
self
|
||||
|
||||
@@ -317,6 +317,26 @@ where
|
||||
type State = ElementState<At::State, Ch::State>;
|
||||
|
||||
fn rebuild(self, state: &mut Self::State) {
|
||||
// check whether the tag is the same, for custom elements
|
||||
// because this is const `false` for all other element types,
|
||||
// the compiler should be able to optimize it out
|
||||
if E::TAG.is_empty() {
|
||||
// see https://github.com/leptos-rs/leptos/issues/4412
|
||||
let new_tag = self.tag.tag();
|
||||
|
||||
// this is not particularly efficient, but it saves us from
|
||||
// having to keep track of the tag name for every element state
|
||||
let old_tag = state.el.tag_name();
|
||||
if new_tag != old_tag {
|
||||
let mut new_state = self.build();
|
||||
state.insert_before_this(&mut new_state);
|
||||
state.unmount();
|
||||
*state = new_state;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// rebuild attributes and children for any element
|
||||
let ElementState {
|
||||
attrs, children, ..
|
||||
} = state;
|
||||
|
||||
@@ -113,7 +113,7 @@ where
|
||||
event,
|
||||
#[cfg(feature = "reactive_graph")]
|
||||
owner: reactive_graph::owner::Owner::current().unwrap_or_default(),
|
||||
cb: Some(SendWrapper::new(cb)),
|
||||
cb: (!cfg!(feature = "ssr")).then(|| SendWrapper::new(cb)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,13 +352,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn dry_resolve(&mut self) {
|
||||
// dry_resolve() only runs during SSR, and we should use it to
|
||||
// synchronously remove and drop the SendWrapper value
|
||||
// we don't need this value during SSR and leaving it here could drop it
|
||||
// from a different thread
|
||||
self.cb.take();
|
||||
}
|
||||
fn dry_resolve(&mut self) {}
|
||||
|
||||
async fn resolve(self) -> Self::AsyncOutput {
|
||||
self
|
||||
|
||||
@@ -22,7 +22,7 @@ where
|
||||
{
|
||||
Property {
|
||||
key,
|
||||
value: Some(SendWrapper::new(value)),
|
||||
value: (!cfg!(feature = "ssr")).then(|| SendWrapper::new(value)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,13 +115,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn dry_resolve(&mut self) {
|
||||
// dry_resolve() only runs during SSR, and we should use it to
|
||||
// synchronously remove and drop the SendWrapper value
|
||||
// we don't need this value during SSR and leaving it here could drop it
|
||||
// from a different thread
|
||||
self.value.take();
|
||||
}
|
||||
fn dry_resolve(&mut self) {}
|
||||
|
||||
async fn resolve(self) -> Self::AsyncOutput {
|
||||
self
|
||||
|
||||
@@ -7,6 +7,9 @@ use std::cell::Cell;
|
||||
use std::{cell::RefCell, panic::Location, rc::Rc};
|
||||
use web_sys::{Comment, Element, Node, Text};
|
||||
|
||||
#[cfg(feature = "mark_branches")]
|
||||
const COMMENT_NODE: u16 = 8;
|
||||
|
||||
/// Hydration works by walking over the DOM, adding interactivity as needed.
|
||||
///
|
||||
/// This cursor tracks the location in the DOM that is currently being hydrated. Each that type
|
||||
@@ -43,13 +46,27 @@ where
|
||||
///
|
||||
/// Does nothing if there is no child.
|
||||
pub fn child(&self) {
|
||||
//crate::log("advancing to next child of ");
|
||||
//Rndr::log_node(&self.current());
|
||||
let mut inner = self.0.borrow_mut();
|
||||
if let Some(node) = Rndr::first_child(&inner) {
|
||||
*inner = node;
|
||||
}
|
||||
//drop(inner);
|
||||
|
||||
#[cfg(feature = "mark_branches")]
|
||||
{
|
||||
while inner.node_type() == COMMENT_NODE {
|
||||
if let Some(content) = inner.text_content() {
|
||||
if content.starts_with("bo") || content.starts_with("bc") {
|
||||
if let Some(sibling) = Rndr::next_sibling(&inner) {
|
||||
*inner = sibling;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
// //drop(inner);
|
||||
//crate::log(">> which is ");
|
||||
//Rndr::log_node(&self.current());
|
||||
}
|
||||
@@ -58,12 +75,25 @@ where
|
||||
///
|
||||
/// Does nothing if there is no sibling.
|
||||
pub fn sibling(&self) {
|
||||
//crate::log("advancing to next sibling of ");
|
||||
//Rndr::log_node(&self.current());
|
||||
let mut inner = self.0.borrow_mut();
|
||||
if let Some(node) = Rndr::next_sibling(&inner) {
|
||||
*inner = node;
|
||||
}
|
||||
|
||||
#[cfg(feature = "mark_branches")]
|
||||
{
|
||||
while inner.node_type() == COMMENT_NODE {
|
||||
if let Some(content) = inner.text_content() {
|
||||
if content.starts_with("bo") || content.starts_with("bc") {
|
||||
if let Some(sibling) = Rndr::next_sibling(&inner) {
|
||||
*inner = sibling;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
//drop(inner);
|
||||
//crate::log(">> which is ");
|
||||
//Rndr::log_node(&self.current());
|
||||
|
||||
@@ -575,15 +575,7 @@ impl RenderHtml for AnyView {
|
||||
#[cfg(feature = "hydrate")]
|
||||
{
|
||||
if FROM_SERVER {
|
||||
if cfg!(feature = "mark_branches") {
|
||||
cursor.advance_to_placeholder(position);
|
||||
}
|
||||
let state =
|
||||
(self.hydrate_from_server)(self.value, cursor, position);
|
||||
if cfg!(feature = "mark_branches") {
|
||||
cursor.advance_to_placeholder(position);
|
||||
}
|
||||
state
|
||||
(self.hydrate_from_server)(self.value, cursor, position)
|
||||
} else {
|
||||
panic!(
|
||||
"hydrating AnyView from inside a ViewTemplate is not \
|
||||
@@ -609,14 +601,8 @@ impl RenderHtml for AnyView {
|
||||
) -> Self::State {
|
||||
#[cfg(feature = "hydrate")]
|
||||
{
|
||||
if cfg!(feature = "mark_branches") {
|
||||
cursor.advance_to_placeholder(position);
|
||||
}
|
||||
let state =
|
||||
(self.hydrate_async)(self.value, cursor, position).await;
|
||||
if cfg!(feature = "mark_branches") {
|
||||
cursor.advance_to_placeholder(position);
|
||||
}
|
||||
state
|
||||
}
|
||||
#[cfg(not(feature = "hydrate"))]
|
||||
|
||||
@@ -411,21 +411,14 @@ where
|
||||
cursor: &Cursor,
|
||||
position: &PositionState,
|
||||
) -> Self::State {
|
||||
if cfg!(feature = "mark_branches") {
|
||||
cursor.advance_to_placeholder(position);
|
||||
}
|
||||
let state = match self {
|
||||
match self {
|
||||
Either::Left(left) => {
|
||||
Either::Left(left.hydrate::<FROM_SERVER>(cursor, position))
|
||||
}
|
||||
Either::Right(right) => {
|
||||
Either::Right(right.hydrate::<FROM_SERVER>(cursor, position))
|
||||
}
|
||||
};
|
||||
if cfg!(feature = "mark_branches") {
|
||||
cursor.advance_to_placeholder(position);
|
||||
}
|
||||
state
|
||||
}
|
||||
|
||||
async fn hydrate_async(
|
||||
@@ -433,21 +426,14 @@ where
|
||||
cursor: &Cursor,
|
||||
position: &PositionState,
|
||||
) -> Self::State {
|
||||
if cfg!(feature = "mark_branches") {
|
||||
cursor.advance_to_placeholder(position);
|
||||
}
|
||||
let state = match self {
|
||||
match self {
|
||||
Either::Left(left) => {
|
||||
Either::Left(left.hydrate_async(cursor, position).await)
|
||||
}
|
||||
Either::Right(right) => {
|
||||
Either::Right(right.hydrate_async(cursor, position).await)
|
||||
}
|
||||
};
|
||||
if cfg!(feature = "mark_branches") {
|
||||
cursor.advance_to_placeholder(position);
|
||||
}
|
||||
state
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
@@ -973,17 +959,11 @@ macro_rules! tuples {
|
||||
cursor: &Cursor,
|
||||
position: &PositionState,
|
||||
) -> Self::State {
|
||||
if cfg!(feature = "mark_branches") {
|
||||
cursor.advance_to_placeholder(position);
|
||||
}
|
||||
let state = match self {
|
||||
$([<EitherOf $num>]::$ty(this) => {
|
||||
[<EitherOf $num>]::$ty(this.hydrate::<FROM_SERVER>(cursor, position))
|
||||
})*
|
||||
};
|
||||
if cfg!(feature = "mark_branches") {
|
||||
cursor.advance_to_placeholder(position);
|
||||
}
|
||||
|
||||
Self::State { state }
|
||||
}
|
||||
@@ -993,17 +973,11 @@ macro_rules! tuples {
|
||||
cursor: &Cursor,
|
||||
position: &PositionState,
|
||||
) -> Self::State {
|
||||
if cfg!(feature = "mark_branches") {
|
||||
cursor.advance_to_placeholder(position);
|
||||
}
|
||||
let state = match self {
|
||||
$([<EitherOf $num>]::$ty(this) => {
|
||||
[<EitherOf $num>]::$ty(this.hydrate_async(cursor, position).await)
|
||||
})*
|
||||
};
|
||||
if cfg!(feature = "mark_branches") {
|
||||
cursor.advance_to_placeholder(position);
|
||||
}
|
||||
|
||||
Self::State { state }
|
||||
}
|
||||
|
||||
@@ -322,10 +322,6 @@ where
|
||||
cursor: &Cursor,
|
||||
position: &PositionState,
|
||||
) -> Self::State {
|
||||
if cfg!(feature = "mark_branches") {
|
||||
cursor.advance_to_placeholder(position);
|
||||
}
|
||||
|
||||
// get parent and position
|
||||
let current = cursor.current();
|
||||
let parent = if position.get() == Position::FirstChild {
|
||||
@@ -346,22 +342,12 @@ where
|
||||
for (index, item) in items.enumerate() {
|
||||
hashed_items.insert((self.key_fn)(&item));
|
||||
let (set_index, view) = (self.view_fn)(index, item);
|
||||
if cfg!(feature = "mark_branches") {
|
||||
cursor.advance_to_placeholder(position);
|
||||
}
|
||||
let item = view.hydrate::<FROM_SERVER>(cursor, position);
|
||||
if cfg!(feature = "mark_branches") {
|
||||
cursor.advance_to_placeholder(position);
|
||||
}
|
||||
rendered_items.push(Some((set_index, item)));
|
||||
}
|
||||
let marker = cursor.next_placeholder(position);
|
||||
position.set(Position::NextChild);
|
||||
|
||||
if cfg!(feature = "mark_branches") {
|
||||
cursor.advance_to_placeholder(position);
|
||||
}
|
||||
|
||||
KeyedState {
|
||||
parent: Some(parent),
|
||||
marker,
|
||||
@@ -375,10 +361,6 @@ where
|
||||
cursor: &Cursor,
|
||||
position: &PositionState,
|
||||
) -> Self::State {
|
||||
if cfg!(feature = "mark_branches") {
|
||||
cursor.advance_to_placeholder(position);
|
||||
}
|
||||
|
||||
// get parent and position
|
||||
let current = cursor.current();
|
||||
let parent = if position.get() == Position::FirstChild {
|
||||
@@ -399,22 +381,12 @@ where
|
||||
for (index, item) in items.enumerate() {
|
||||
hashed_items.insert((self.key_fn)(&item));
|
||||
let (set_index, view) = (self.view_fn)(index, item);
|
||||
if cfg!(feature = "mark_branches") {
|
||||
cursor.advance_to_placeholder(position);
|
||||
}
|
||||
let item = view.hydrate_async(cursor, position).await;
|
||||
if cfg!(feature = "mark_branches") {
|
||||
cursor.advance_to_placeholder(position);
|
||||
}
|
||||
rendered_items.push(Some((set_index, item)));
|
||||
}
|
||||
let marker = cursor.next_placeholder(position);
|
||||
position.set(Position::NextChild);
|
||||
|
||||
if cfg!(feature = "mark_branches") {
|
||||
cursor.advance_to_placeholder(position);
|
||||
}
|
||||
|
||||
KeyedState {
|
||||
parent: Some(parent),
|
||||
marker,
|
||||
@@ -642,18 +614,13 @@ struct DiffOpRemove {
|
||||
at: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||
enum DiffOpAddMode {
|
||||
#[default]
|
||||
Normal,
|
||||
Append,
|
||||
}
|
||||
|
||||
impl Default for DiffOpAddMode {
|
||||
fn default() -> Self {
|
||||
Self::Normal
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_diff<T, VFS, V>(
|
||||
parent: Option<&crate::renderer::types::Element>,
|
||||
marker: &crate::renderer::types::Placeholder,
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
[package]
|
||||
name = "wasm_split_helpers"
|
||||
version = "0.1.2"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Tools to support code-splitting and lazy loading for WebAssembly (WASM) binaries."
|
||||
rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
async-once-cell = { default-features = true, workspace = true, features = [
|
||||
"std",
|
||||
] }
|
||||
wasm_split_macros.workspace = true
|
||||
or_poisoned.workspace = true
|
||||
@@ -1 +0,0 @@
|
||||
extend = { path = "../cargo-make/main.toml" }
|
||||
@@ -1,9 +0,0 @@
|
||||
# `wasm_split_helpers`
|
||||
|
||||
This crate provides functions that are used by the `wasm_split_macros` crate, which allows you to indicate that certain functions are appropriate split points for lazy-loaded code.
|
||||
|
||||
A build tool that supports this approach (like `cargo-leptos`) can then split a WebAssembly (WASM) binary into multiple chunks, which will be lazy-loaded when a split function is called.
|
||||
|
||||
This crate was adapted from an original prototype, which you can find [here](https://github.com/jbms/wasm-split-prototype), with an in-depth description of the approach [here](https://github.com/rustwasm/wasm-bindgen/issues/3939).
|
||||
|
||||
This functionality is provided in Leptos by the `#[lazy]` and `#[lazy_route]` macros.
|
||||
@@ -1,107 +0,0 @@
|
||||
use std::{
|
||||
ffi::c_void,
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
sync::{Arc, Mutex},
|
||||
task::{Context, Poll, Waker},
|
||||
};
|
||||
|
||||
pub type LoadCallbackFn = unsafe extern "C" fn(*const c_void, bool) -> ();
|
||||
pub type LoadFn = unsafe extern "C" fn(LoadCallbackFn, *const c_void) -> ();
|
||||
|
||||
type Lazy = async_once_cell::Lazy<Option<()>, SplitLoaderFuture>;
|
||||
|
||||
use or_poisoned::OrPoisoned;
|
||||
pub use wasm_split_macros::wasm_split;
|
||||
|
||||
pub struct LazySplitLoader {
|
||||
lazy: Pin<Arc<Lazy>>,
|
||||
}
|
||||
|
||||
impl LazySplitLoader {
|
||||
pub fn new(load: LoadFn) -> Self {
|
||||
Self {
|
||||
lazy: Arc::pin(Lazy::new(SplitLoaderFuture::new(
|
||||
SplitLoader::new(load),
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn ensure_loaded(
|
||||
loader: &'static std::thread::LocalKey<LazySplitLoader>,
|
||||
) -> Option<()> {
|
||||
*loader.with(|inner| inner.lazy.clone()).as_ref().await
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum SplitLoaderState {
|
||||
Deferred(LoadFn),
|
||||
Pending,
|
||||
Completed(Option<()>),
|
||||
}
|
||||
|
||||
struct SplitLoader {
|
||||
state: Mutex<SplitLoaderState>,
|
||||
waker: Mutex<Option<Waker>>,
|
||||
}
|
||||
|
||||
impl SplitLoader {
|
||||
fn new(load: LoadFn) -> Arc<Self> {
|
||||
Arc::new(SplitLoader {
|
||||
state: Mutex::new(SplitLoaderState::Deferred(load)),
|
||||
waker: Mutex::new(None),
|
||||
})
|
||||
}
|
||||
|
||||
fn complete(&self, value: bool) {
|
||||
*self.state.lock().or_poisoned() =
|
||||
SplitLoaderState::Completed(if value { Some(()) } else { None });
|
||||
if let Some(waker) = self.waker.lock().or_poisoned().take() {
|
||||
waker.wake();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SplitLoaderFuture {
|
||||
loader: Arc<SplitLoader>,
|
||||
}
|
||||
|
||||
impl SplitLoaderFuture {
|
||||
fn new(loader: Arc<SplitLoader>) -> Self {
|
||||
SplitLoaderFuture { loader }
|
||||
}
|
||||
}
|
||||
|
||||
impl Future for SplitLoaderFuture {
|
||||
type Output = Option<()>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<()>> {
|
||||
let mut loader = self.loader.state.lock().or_poisoned();
|
||||
match *loader {
|
||||
SplitLoaderState::Deferred(load) => {
|
||||
*loader = SplitLoaderState::Pending;
|
||||
*self.loader.waker.lock().or_poisoned() =
|
||||
Some(cx.waker().clone());
|
||||
unsafe {
|
||||
load(
|
||||
load_callback,
|
||||
Arc::<SplitLoader>::into_raw(self.loader.clone())
|
||||
as *const c_void,
|
||||
)
|
||||
};
|
||||
Poll::Pending
|
||||
}
|
||||
SplitLoaderState::Pending => {
|
||||
*self.loader.waker.lock().or_poisoned() =
|
||||
Some(cx.waker().clone());
|
||||
Poll::Pending
|
||||
}
|
||||
SplitLoaderState::Completed(value) => Poll::Ready(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" fn load_callback(loader: *const c_void, success: bool) {
|
||||
unsafe { Arc::from_raw(loader as *const SplitLoader) }.complete(success);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
[package]
|
||||
name = "wasm_split_macros"
|
||||
version = "0.1.3"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Tools to support code-splitting and lazy loading for WebAssembly (WASM) binaries."
|
||||
rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
base16 = { workspace = true, default-features = true }
|
||||
digest = { workspace = true, default-features = true }
|
||||
quote = { workspace = true, default-features = true }
|
||||
sha2 = { workspace = true, default-features = true }
|
||||
syn = { workspace = true, default-features = true }
|
||||
wasm-bindgen = { workspace = true, default-features = true }
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
@@ -1 +0,0 @@
|
||||
extend = { path = "../cargo-make/main.toml" }
|
||||
@@ -1,9 +0,0 @@
|
||||
# `wasm_split_macros`
|
||||
|
||||
This crate provides macros that are used along with the `wasm_split_helpers` crate, which allows you to indicate that certain functions are appropriate split points for lazy-loaded code.
|
||||
|
||||
A build tool that supports this approach (like `cargo-leptos`) can then split a WebAssembly (WASM) binary into multiple chunks, which will be lazy-loaded when a split function is called.
|
||||
|
||||
This crate was adapted from an original prototype, which you can find [here](https://github.com/jbms/wasm-split-prototype), with an in-depth description of the approach [here](https://github.com/rustwasm/wasm-bindgen/issues/3939).
|
||||
|
||||
This functionality is provided in Leptos by the `#[lazy]` and `#[lazy_route]` macros.
|
||||
@@ -1,172 +0,0 @@
|
||||
use digest::Digest;
|
||||
use proc_macro::TokenStream;
|
||||
use quote::{format_ident, quote};
|
||||
use syn::{
|
||||
parse,
|
||||
parse::{Parse, ParseStream},
|
||||
parse_macro_input,
|
||||
token::Comma,
|
||||
Ident, ItemFn, Path, Result, ReturnType, Signature,
|
||||
};
|
||||
|
||||
struct WasmSplitArgs {
|
||||
module_ident: Ident,
|
||||
_comma: Option<Comma>,
|
||||
send_wrapper_path: Option<Path>,
|
||||
}
|
||||
|
||||
impl Parse for WasmSplitArgs {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
let module_ident = input.parse()?;
|
||||
let _comma = input.parse().ok();
|
||||
let send_wrapper_path = input.parse().ok();
|
||||
Ok(Self {
|
||||
module_ident,
|
||||
_comma,
|
||||
send_wrapper_path,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn wasm_split(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
let WasmSplitArgs {
|
||||
module_ident,
|
||||
send_wrapper_path,
|
||||
..
|
||||
} = parse_macro_input!(args);
|
||||
let item_fn = parse_macro_input!(input as ItemFn);
|
||||
|
||||
let name = &item_fn.sig.ident;
|
||||
|
||||
let preload_name =
|
||||
Ident::new(&format!("__preload_{}", item_fn.sig.ident), name.span());
|
||||
|
||||
let unique_identifier = base16::encode_lower(
|
||||
&sha2::Sha256::digest(format!("{name} {span:?}", span = name.span()))
|
||||
[..16],
|
||||
);
|
||||
|
||||
let load_module_ident = format_ident!("__wasm_split_load_{module_ident}");
|
||||
let split_loader_ident =
|
||||
format_ident!("__wasm_split_loader_{unique_identifier}");
|
||||
let impl_import_ident = format_ident!(
|
||||
"__wasm_split_00{module_ident}00_import_{unique_identifier}_{name}"
|
||||
);
|
||||
let impl_export_ident = format_ident!(
|
||||
"__wasm_split_00{module_ident}00_export_{unique_identifier}_{name}"
|
||||
);
|
||||
|
||||
let mut import_sig = Signature {
|
||||
ident: impl_import_ident.clone(),
|
||||
asyncness: None,
|
||||
..item_fn.sig.clone()
|
||||
};
|
||||
let mut export_sig = Signature {
|
||||
ident: impl_export_ident.clone(),
|
||||
asyncness: None,
|
||||
..item_fn.sig.clone()
|
||||
};
|
||||
|
||||
let was_async = item_fn.sig.asyncness.is_some();
|
||||
if was_async {
|
||||
let ty = match &item_fn.sig.output {
|
||||
ReturnType::Default => quote! { () },
|
||||
ReturnType::Type(_, ty) => quote! { #ty },
|
||||
};
|
||||
let async_output = parse::<ReturnType>(
|
||||
quote! {
|
||||
-> std::pin::Pin<Box<dyn std::future::Future<Output = #ty> + Send + Sync>>
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.unwrap();
|
||||
export_sig.output = async_output.clone();
|
||||
import_sig.output = async_output;
|
||||
}
|
||||
|
||||
let wrapper_pub = item_fn.vis;
|
||||
let mut wrapper_sig = item_fn.sig;
|
||||
wrapper_sig.asyncness = Some(Default::default());
|
||||
let mut args = Vec::new();
|
||||
for (i, param) in wrapper_sig.inputs.iter_mut().enumerate() {
|
||||
match param {
|
||||
syn::FnArg::Typed(pat_type) => {
|
||||
let param_ident = format_ident!("__wasm_split_arg_{i}");
|
||||
args.push(param_ident.clone());
|
||||
pat_type.pat = Box::new(syn::Pat::Ident(syn::PatIdent {
|
||||
attrs: vec![],
|
||||
by_ref: None,
|
||||
mutability: None,
|
||||
ident: param_ident,
|
||||
subpat: None,
|
||||
}));
|
||||
}
|
||||
syn::FnArg::Receiver(_) => {
|
||||
args.push(format_ident!("self"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let attrs = item_fn.attrs;
|
||||
|
||||
let stmts = &item_fn.block.stmts;
|
||||
|
||||
let body = if was_async {
|
||||
if let Some(send_wrapper_path) = send_wrapper_path {
|
||||
quote! {
|
||||
Box::pin(#send_wrapper_path::SendWrapper::new(async move {
|
||||
#(#stmts)*
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
Box::pin(async move {
|
||||
#(#stmts)*
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! { #(#stmts)* }
|
||||
};
|
||||
|
||||
let await_result = was_async.then(|| quote! { .await });
|
||||
|
||||
quote! {
|
||||
thread_local! {
|
||||
static #split_loader_ident: ::leptos::wasm_split_helpers::LazySplitLoader = ::leptos::wasm_split_helpers::LazySplitLoader::new(#load_module_ident);
|
||||
}
|
||||
|
||||
#[link(wasm_import_module = "/pkg/__wasm_split.______________________.js")]
|
||||
extern "C" {
|
||||
#[no_mangle]
|
||||
fn #load_module_ident (callback: unsafe extern "C" fn(*const ::std::ffi::c_void, bool), data: *const ::std::ffi::c_void) -> ();
|
||||
|
||||
#[allow(improper_ctypes)]
|
||||
#[no_mangle]
|
||||
#import_sig;
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#(#attrs)*
|
||||
#wrapper_pub #wrapper_sig {
|
||||
#(#attrs)*
|
||||
#[allow(improper_ctypes_definitions)]
|
||||
#[allow(non_snake_case)]
|
||||
#[no_mangle]
|
||||
pub extern "C" #export_sig {
|
||||
#body
|
||||
}
|
||||
|
||||
::leptos::wasm_split_helpers::ensure_loaded(&#split_loader_ident).await.unwrap();
|
||||
unsafe { #impl_import_ident( #(#args),* ) } #await_result
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[allow(non_snake_case)]
|
||||
#wrapper_pub async fn #preload_name() {
|
||||
::leptos::wasm_split_helpers::ensure_loaded(&#split_loader_ident).await.unwrap();
|
||||
}
|
||||
}
|
||||
.into()
|
||||
}
|
||||
Reference in New Issue
Block a user