mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 15:44:42 -05:00
Compare commits
192 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1524386346 | ||
|
|
426b079709 | ||
|
|
c6f176e2b0 | ||
|
|
75662d08e7 | ||
|
|
4448b77cde | ||
|
|
956af8e466 | ||
|
|
8c469b85d6 | ||
|
|
7f93dd224d | ||
|
|
777b5e1e54 | ||
|
|
433f7284e6 | ||
|
|
4a8a212d84 | ||
|
|
1d7bc021af | ||
|
|
74055a7e13 | ||
|
|
c98082de74 | ||
|
|
b8d44e20a9 | ||
|
|
00e83e0d70 | ||
|
|
e89b1389ca | ||
|
|
bd454d03e2 | ||
|
|
d7f4457ea4 | ||
|
|
17d357bcec | ||
|
|
66d1bead9a | ||
|
|
69c918e813 | ||
|
|
2817a261ce | ||
|
|
972b1ff90b | ||
|
|
3a66a1f3d3 | ||
|
|
504f983996 | ||
|
|
0862385816 | ||
|
|
8319446d3f | ||
|
|
5fa31941bb | ||
|
|
f4bb87ea1e | ||
|
|
016fbf8da1 | ||
|
|
21fd995468 | ||
|
|
683e7177dd | ||
|
|
33b278c014 | ||
|
|
5fc56346f4 | ||
|
|
afb37aaf4b | ||
|
|
f8fd79725a | ||
|
|
131251b361 | ||
|
|
91fb315fe0 | ||
|
|
6954b77b62 | ||
|
|
77176f8395 | ||
|
|
344b79a01b | ||
|
|
051059c761 | ||
|
|
3c540dd858 | ||
|
|
4125688a0a | ||
|
|
bd3b962cfb | ||
|
|
5dd3c217c4 | ||
|
|
ae00e5ae13 | ||
|
|
1ce671ba08 | ||
|
|
ec9f26bd9f | ||
|
|
831eae31bc | ||
|
|
ff6ae5de25 | ||
|
|
c21712ba04 | ||
|
|
45771b6fd3 | ||
|
|
f3557970a7 | ||
|
|
c87ef331b0 | ||
|
|
e767518142 | ||
|
|
f94b681118 | ||
|
|
9c50e49253 | ||
|
|
57c7097ede | ||
|
|
1a06e0eee8 | ||
|
|
ce9af4a685 | ||
|
|
e0c79eb8d8 | ||
|
|
9fd972971e | ||
|
|
9473220639 | ||
|
|
ae11812dc6 | ||
|
|
4c55c25445 | ||
|
|
649b5fbe9e | ||
|
|
adb3e75efc | ||
|
|
f303aa6d5c | ||
|
|
73ca3d7b04 | ||
|
|
235393bfbe | ||
|
|
17d8e2bd09 | ||
|
|
f51c676e0d | ||
|
|
cf0aa0e4d7 | ||
|
|
df09d4a7f6 | ||
|
|
30b0a579ca | ||
|
|
50a4c3b0d9 | ||
|
|
c76649d77b | ||
|
|
911be5007e | ||
|
|
5227221c96 | ||
|
|
3f48b77256 | ||
|
|
99117f496f | ||
|
|
cf12ea3404 | ||
|
|
d555c1e0ce | ||
|
|
40ea20057f | ||
|
|
5587ccd1eb | ||
|
|
50a9df9eea | ||
|
|
c46b1c4e25 | ||
|
|
e6f86408a1 | ||
|
|
aa13ed9431 | ||
|
|
607a7987e5 | ||
|
|
c0c3279cbb | ||
|
|
ece6d9dd93 | ||
|
|
74ecf4763a | ||
|
|
2c5c69c2fe | ||
|
|
0c275d6540 | ||
|
|
6be3266a2e | ||
|
|
c3efb8e476 | ||
|
|
32e0551b10 | ||
|
|
671ada36ab | ||
|
|
a9ab4ea372 | ||
|
|
1d72b75d03 | ||
|
|
798d8a4a9e | ||
|
|
f4e0be2d59 | ||
|
|
05f50f7d27 | ||
|
|
a22d6f58be | ||
|
|
ff21c9cae2 | ||
|
|
726b7b3116 | ||
|
|
6e91b6fada | ||
|
|
76f1c7a50c | ||
|
|
733a353820 | ||
|
|
829b07b598 | ||
|
|
8c6059774f | ||
|
|
0e65034b01 | ||
|
|
e1549c5ab3 | ||
|
|
624e91bb2a | ||
|
|
0df6cd74ee | ||
|
|
1da833a0aa | ||
|
|
f37d124d6a | ||
|
|
5d0e683b0f | ||
|
|
f34e3a5bc9 | ||
|
|
d7dd6a1109 | ||
|
|
ff81d34084 | ||
|
|
40a7aba3bc | ||
|
|
d4dcafd908 | ||
|
|
82ccbbf806 | ||
|
|
5ba45bb1ed | ||
|
|
06dfa37eee | ||
|
|
e82a0bbc7f | ||
|
|
4a972fc09e | ||
|
|
07cf649e3b | ||
|
|
0e9598b799 | ||
|
|
82303d7e33 | ||
|
|
c4354ac965 | ||
|
|
7de550685a | ||
|
|
b1f3f6023e | ||
|
|
c189c3a45d | ||
|
|
3903867f82 | ||
|
|
a42fa452fc | ||
|
|
cd48a6ac8c | ||
|
|
34c14adcb8 | ||
|
|
50cee1d614 | ||
|
|
7ca691305f | ||
|
|
830882f330 | ||
|
|
13110a35e2 | ||
|
|
304dc081a2 | ||
|
|
14f6bc658e | ||
|
|
09894aaca9 | ||
|
|
2ee4444bb4 | ||
|
|
03a1c1e7a6 | ||
|
|
12e49ed996 | ||
|
|
1e281e9e74 | ||
|
|
bd475f89d0 | ||
|
|
3d91b5e90f | ||
|
|
96d8d5218c | ||
|
|
84caa35cef | ||
|
|
fc8b55161c | ||
|
|
657052466b | ||
|
|
efe8336363 | ||
|
|
770881842c | ||
|
|
0d540ef02f | ||
|
|
dc1885ad92 | ||
|
|
61bf87439a | ||
|
|
308568e520 | ||
|
|
1b0f32dc4c | ||
|
|
2e0b3011d9 | ||
|
|
680d4ccd07 | ||
|
|
325f9cbe33 | ||
|
|
26ab392c95 | ||
|
|
3a4e2a19aa | ||
|
|
eed3d21b40 | ||
|
|
7ae386285d | ||
|
|
ebcc51136d | ||
|
|
e10ded4fd0 | ||
|
|
67be872f58 | ||
|
|
e5b21ac0fc | ||
|
|
9b2e313d20 | ||
|
|
bc79232033 | ||
|
|
a7bb2565c4 | ||
|
|
2e393aaca0 | ||
|
|
e37711cb85 | ||
|
|
627b553e60 | ||
|
|
e2f9aca466 | ||
|
|
970544ed0b | ||
|
|
0c3b3c440f | ||
|
|
6578086e09 | ||
|
|
113aba9666 | ||
|
|
58d7475193 | ||
|
|
04d80ff8d0 | ||
|
|
7971a2dccb | ||
|
|
e2ea4277bc |
9
.github/ISSUE_TEMPLATE/bug_report.md
vendored
9
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -33,10 +33,11 @@ Steps to reproduce the behavior:
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Next Steps**
|
||||
[ ] I will make a PR
|
||||
[ ] I would like to make a PR, but need help getting started
|
||||
[ ] I want someone else to take the time to fix this
|
||||
[ ] This is a low priority for me and is just shared for your information
|
||||
|
||||
- [ ] I will make a PR
|
||||
- [ ] I would like to make a PR, but need help getting started
|
||||
- [ ] I want someone else to take the time to fix this
|
||||
- [ ] This is a low priority for me and is just shared for your information
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
||||
19
.github/dependabot.yml
vendored
Normal file
19
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
# Grouping all dependencies in one PR weekly
|
||||
- package-ecosystem: cargo
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: monday
|
||||
open-pull-requests-limit: 1
|
||||
allow:
|
||||
- dependency-type: "all"
|
||||
groups:
|
||||
rust-dependencies:
|
||||
patterns:
|
||||
- "*"
|
||||
36
.github/workflows/autofix.yml
vendored
36
.github/workflows/autofix.yml
vendored
@@ -21,34 +21,26 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with: {toolchain: nightly, components: "rustfmt, clippy", target: "wasm32-unknown-unknown", rustflags: ""}
|
||||
with:
|
||||
{
|
||||
toolchain: "nightly-2025-07-16",
|
||||
components: "rustfmt, clippy",
|
||||
target: "wasm32-unknown-unknown",
|
||||
rustflags: "",
|
||||
}
|
||||
- name: Install Glib
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libglib2.0-dev
|
||||
- name: Install cargo-all-features
|
||||
run: cargo install --git https://github.com/sabify/cargo-all-features --branch arbitrary-command-support
|
||||
- name: Install jq
|
||||
run: sudo apt-get install jq
|
||||
- run: |
|
||||
echo "Formatting the workspace"
|
||||
cargo fmt --all
|
||||
|
||||
echo "Running Clippy against each member's features (default features included)"
|
||||
for member in $(cargo metadata --no-deps --format-version 1 | jq -r '.packages[] | .name'); do
|
||||
echo "Working on member $member":
|
||||
echo -e "\tdefault-features/no-features:"
|
||||
# this will also run on members with no features or default features
|
||||
cargo clippy --allow-dirty --fix --lib --package "$member"
|
||||
|
||||
features=$(cargo metadata --no-deps --format-version 1 | jq -r ".packages[] | select(.name == \"$member\") | .features | keys[]")
|
||||
for feature in $features; do
|
||||
if [ "$feature" = "default" ]; then
|
||||
continue
|
||||
fi
|
||||
echo -e "\tfeature $feature"
|
||||
cargo clippy --allow-dirty --fix --lib --package "$member" --features "$feature"
|
||||
done
|
||||
done
|
||||
- uses: autofix-ci/action@v1.3.1
|
||||
- name: Format the workspace
|
||||
run: cargo fmt --all
|
||||
- name: Clippy the workspace
|
||||
run: cargo all-features clippy --allow-dirty --fix --lib --no-deps
|
||||
- uses: autofix-ci/action@v1.3.2
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
fail-fast: false
|
||||
|
||||
22
.github/workflows/run-cargo-make-task.yml
vendored
22
.github/workflows/run-cargo-make-task.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
toolchain: [stable, nightly-2025-03-05]
|
||||
toolchain: [stable, nightly-2025-07-16]
|
||||
erased_mode: [true, false]
|
||||
steps:
|
||||
- name: Free Disk Space
|
||||
@@ -72,6 +72,14 @@ jobs:
|
||||
run: cargo binstall cargo-nextest --no-confirm
|
||||
- name: Install cargo-all-features
|
||||
run: cargo install --git https://github.com/sabify/cargo-all-features --branch arbitrary-command-support
|
||||
# Part of direct-minimal-versions check
|
||||
- name: Install cargo-hack
|
||||
if: contains(matrix.toolchain, 'nightly')
|
||||
uses: taiki-e/install-action@cargo-hack
|
||||
# Part of direct-minimal-versions check
|
||||
- name: Install cargo-minimal-versions
|
||||
if: contains(matrix.toolchain, 'nightly')
|
||||
uses: taiki-e/install-action@cargo-minimal-versions
|
||||
- name: Install Trunk
|
||||
if: contains(inputs.directory, 'examples')
|
||||
run: cargo binstall trunk --no-confirm
|
||||
@@ -160,6 +168,18 @@ jobs:
|
||||
run: |
|
||||
cd '${{ inputs.directory }}'
|
||||
cargo make --no-workspace --profile=github-actions ci
|
||||
# check the direct-minimal-versions on release
|
||||
COMMIT_MSG=$(git log -1 --pretty=format:'%s')
|
||||
# Supports: v1.2.3, v1.2.3-alpha, v1.2.3-beta1, v1.2.3-rc.1, etc.
|
||||
if [[ "$COMMIT_MSG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+(\.?[0-9]+)?)?$ ]]; then
|
||||
cargo make --no-workspace --profile=github-actions check-minimal-versions
|
||||
fi
|
||||
# Check if the counter_isomorphic can be built with leptos_debuginfo cfg flag in release mode
|
||||
- name: ${{ inputs.cargo_make_task }} with --cfg=leptos_debuginfo
|
||||
if: contains(inputs.directory, 'counter_isomorphic')
|
||||
run: |
|
||||
cd '${{ inputs.directory }}'
|
||||
RUSTFLAGS="$RUSTFLAGS --cfg leptos_debuginfo" cargo leptos build --release
|
||||
- name: Clean up ${{ inputs.directory }}
|
||||
if: always()
|
||||
run: |
|
||||
|
||||
1025
Cargo.lock
generated
1025
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
145
Cargo.toml
145
Cargo.toml
@@ -40,44 +40,131 @@ members = [
|
||||
exclude = ["benchmarks", "examples", "projects"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.8.0-rc3"
|
||||
version = "0.8.4"
|
||||
edition = "2021"
|
||||
rust-version = "1.76"
|
||||
rust-version = "1.88"
|
||||
|
||||
[workspace.dependencies]
|
||||
convert_case = "0.8"
|
||||
# members
|
||||
throw_error = { path = "./any_error/", version = "0.3.0" }
|
||||
any_spawner = { path = "./any_spawner/", version = "0.3.0-rc3" }
|
||||
any_spawner = { path = "./any_spawner/", version = "0.3.0" }
|
||||
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1" }
|
||||
either_of = { path = "./either_of/", version = "0.1.5" }
|
||||
either_of = { path = "./either_of/", version = "0.1.6" }
|
||||
hydration_context = { path = "./hydration_context", version = "0.3.0" }
|
||||
itertools = "0.14.0"
|
||||
leptos = { path = "./leptos", version = "0.8.0-rc3" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.8.0-rc3" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.8.0-rc3" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.0-rc3" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.8.0-rc3" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.8.0-rc3" }
|
||||
leptos_router = { path = "./router", version = "0.8.0-rc3" }
|
||||
leptos_router_macro = { path = "./router_macro", version = "0.8.0-rc3" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.8.0-rc3" }
|
||||
leptos_meta = { path = "./meta", version = "0.8.0-rc3" }
|
||||
leptos = { path = "./leptos", version = "0.8.4" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.8.4" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.8.4" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.4" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.8.4" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.8.4" }
|
||||
leptos_router = { path = "./router", version = "0.8.4" }
|
||||
leptos_router_macro = { path = "./router_macro", version = "0.8.4" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.8.4" }
|
||||
leptos_meta = { path = "./meta", version = "0.8.4" }
|
||||
next_tuple = { path = "./next_tuple", version = "0.1.0" }
|
||||
oco_ref = { path = "./oco", version = "0.2.0" }
|
||||
or_poisoned = { path = "./or_poisoned", version = "0.1.0" }
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.2.0-rc3" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.2.0-rc3" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.2.0-rc3" }
|
||||
rustversion = "1"
|
||||
serde_json = "1.0.0"
|
||||
server_fn = { path = "./server_fn", version = "0.8.0-rc3" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.8.0-rc3" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.0-rc3" }
|
||||
tachys = { path = "./tachys", version = "0.2.0-rc3" }
|
||||
trybuild = "1"
|
||||
typed-builder = "0.21.0"
|
||||
thiserror = "2.0.12"
|
||||
wasm-bindgen = "0.2.100"
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.2.4" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.2.4" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.2.4" }
|
||||
server_fn = { path = "./server_fn", version = "0.8.4" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.8.4" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.4" }
|
||||
tachys = { path = "./tachys", version = "0.2.5" }
|
||||
|
||||
# members deps
|
||||
itertools = { default-features = false, version = "0.14.0" }
|
||||
convert_case = { default-features = false, version = "0.8.0" }
|
||||
serde_json = { default-features = false, version = "1.0.140" }
|
||||
trybuild = { default-features = false, version = "1.0.106" }
|
||||
typed-builder = { default-features = false, version = "0.21.0" }
|
||||
thiserror = { default-features = false, version = "2.0.12" }
|
||||
wasm-bindgen = { default-features = false, version = "0.2.100" }
|
||||
indexmap = { default-features = false, version = "2.9.0" }
|
||||
rstml = { default-features = false, version = "0.12.1" }
|
||||
rustc_version = { default-features = false, version = "0.4.1" }
|
||||
guardian = { default-features = false, version = "1.3.0" }
|
||||
rustc-hash = { default-features = false, version = "2.1.1" }
|
||||
actix-web = { default-features = false, version = "4.11.0" }
|
||||
tracing = { default-features = false, version = "0.1.41" }
|
||||
slotmap = { default-features = false, version = "1.0.7" }
|
||||
futures = { default-features = false, version = "0.3.31" }
|
||||
dashmap = { default-features = false, version = "6.1.0" }
|
||||
pin-project-lite = { default-features = false, version = "0.2.16" }
|
||||
send_wrapper = { default-features = false, version = "0.6.0" }
|
||||
tokio-test = { default-features = false, version = "0.4.4" }
|
||||
html-escape = { default-features = false, version = "0.2.13" }
|
||||
proc-macro-error2 = { default-features = false, version = "2.0.1" }
|
||||
const_format = { default-features = false, version = "0.2.34" }
|
||||
gloo-net = { default-features = false, version = "0.6.0" }
|
||||
url = { default-features = false, version = "2.5.4" }
|
||||
tokio = { default-features = false, version = "1.46.1" }
|
||||
base64 = { default-features = false, version = "0.22.1" }
|
||||
cfg-if = { default-features = false, version = "1.0.0" }
|
||||
wasm-bindgen-futures = { default-features = false, version = "0.4.50" }
|
||||
tower = { default-features = false, version = "0.5.2" }
|
||||
proc-macro2 = { default-features = false, version = "1.0.95" }
|
||||
serde = { default-features = false, version = "1.0.219" }
|
||||
parking_lot = { default-features = false, version = "0.12.4" }
|
||||
axum = { default-features = false, version = "0.8.4" }
|
||||
serde_qs = { default-features = false, version = "0.15.0" }
|
||||
syn = { default-features = false, version = "2.0.104" }
|
||||
xxhash-rust = { default-features = false, version = "0.8.15" }
|
||||
paste = { default-features = false, version = "1.0.15" }
|
||||
quote = { default-features = false, version = "1.0.40" }
|
||||
web-sys = { default-features = false, version = "0.3.77" }
|
||||
js-sys = { default-features = false, version = "0.3.77" }
|
||||
rand = { default-features = false, version = "0.9.1" }
|
||||
serde-lite = { default-features = false, version = "0.5.0" }
|
||||
tokio-tungstenite = { default-features = false, version = "0.27.0" }
|
||||
serial_test = { default-features = false, version = "3.2.0" }
|
||||
erased = { default-features = false, version = "0.1.2" }
|
||||
glib = { default-features = false, version = "0.20.12" }
|
||||
async-trait = { default-features = false, version = "0.1.88" }
|
||||
typed-builder-macro = { default-features = false, version = "0.21.0" }
|
||||
linear-map = { default-features = false, version = "1.2.0" }
|
||||
anyhow = { default-features = false, version = "1.0.98" }
|
||||
walkdir = { default-features = false, version = "2.5.0" }
|
||||
actix-ws = { default-features = false, version = "0.3.0" }
|
||||
tower-http = { default-features = false, version = "0.6.4" }
|
||||
prettyplease = { default-features = false, version = "0.2.35" }
|
||||
inventory = { default-features = false, version = "0.3.20" }
|
||||
config = { default-features = false, version = "0.15.13" }
|
||||
camino = { default-features = false, version = "1.1.9" }
|
||||
ciborium = { default-features = false, version = "0.2.2" }
|
||||
multer = { default-features = false, version = "3.1.0" }
|
||||
leptos-spin-macro = { default-features = false, version = "0.2.0" }
|
||||
sledgehammer_utils = { default-features = false, version = "0.3.1" }
|
||||
sledgehammer_bindgen = { default-features = false, version = "0.6.0" }
|
||||
wasm-streams = { default-features = false, version = "0.4.2" }
|
||||
rkyv = { default-features = false, version = "0.8.10" }
|
||||
temp-env = { default-features = false, version = "0.3.6" }
|
||||
uuid = { default-features = false, version = "1.17.0" }
|
||||
bytes = { default-features = false, version = "1.10.1" }
|
||||
http = { default-features = false, version = "1.3.1" }
|
||||
regex = { default-features = false, version = "1.11.1" }
|
||||
drain_filter_polyfill = { default-features = false, version = "0.1.3" }
|
||||
tempfile = { default-features = false, version = "3.20.0" }
|
||||
futures-lite = { default-features = false, version = "2.6.0" }
|
||||
log = { default-features = false, version = "0.4.27" }
|
||||
percent-encoding = { default-features = false, version = "2.3.1" }
|
||||
async-executor = { default-features = false, version = "1.13.2" }
|
||||
const-str = { default-features = false, version = "0.6.3" }
|
||||
http-body-util = { default-features = false, version = "0.1.3" }
|
||||
hyper = { default-features = false, version = "1.6.0" }
|
||||
postcard = { default-features = false, version = "1.1.1" }
|
||||
rmp-serde = { default-features = false, version = "1.3.0" }
|
||||
reqwest = { default-features = false, version = "0.12.22" }
|
||||
tower-layer = { default-features = false, version = "0.3.3" }
|
||||
attribute-derive = { default-features = false, version = "0.10.3" }
|
||||
insta = { default-features = false, version = "1.43.1" }
|
||||
codee = { default-features = false, version = "0.3.0" }
|
||||
actix-http = { default-features = false, version = "3.11.0" }
|
||||
wasm-bindgen-test = { default-features = false, version = "0.3.50" }
|
||||
rustversion = { default-features = false, version = "1.0.21" }
|
||||
getrandom = { default-features = false, version = "0.3.3" }
|
||||
actix-files = { default-features = false, version = "0.6.6" }
|
||||
async-lock = { default-features = false, version = "3.4.0" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
26
README.md
26
README.md
@@ -90,35 +90,13 @@ Here are some resources for learning more about Leptos:
|
||||
- [API Documentation](https://docs.rs/leptos/latest/leptos/)
|
||||
- [Common Bugs](https://github.com/leptos-rs/leptos/tree/main/docs/COMMON_BUGS.md) (and how to fix them!)
|
||||
|
||||
## `nightly` Note
|
||||
|
||||
Most of the examples assume you’re using `nightly` version of Rust and the `nightly` feature of Leptos. To use `nightly` Rust, you can either set your toolchain globally or on per-project basis.
|
||||
|
||||
To set `nightly` as a default toolchain for all projects (and add the ability to compile Rust to WebAssembly, if you haven’t already):
|
||||
|
||||
```
|
||||
rustup toolchain install nightly
|
||||
rustup default nightly
|
||||
rustup target add wasm32-unknown-unknown
|
||||
```
|
||||
|
||||
If you'd like to use `nightly` only in your Leptos project however, add [`rust-toolchain.toml`](https://rust-lang.github.io/rustup/overrides.html#the-toolchain-file) file with the following content:
|
||||
|
||||
```toml
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
targets = ["wasm32-unknown-unknown"]
|
||||
```
|
||||
|
||||
The `nightly` feature enables the function call syntax for accessing and setting signals, as opposed to `.get()` and `.set()`. This leads to a consistent mental model in which accessing a reactive value of any kind (a signal, memo, or derived signal) is always represented as a function call. This is only possible with nightly Rust and the `nightly` feature.
|
||||
|
||||
## `cargo-leptos`
|
||||
|
||||
[`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos) is a build tool that's designed to make it easy to build apps that run on both the client and the server, with seamless integration. The best way to get started with a real Leptos project right now is to use `cargo-leptos` and our starter templates for [Actix](https://github.com/leptos-rs/start) or [Axum](https://github.com/leptos-rs/start-axum).
|
||||
|
||||
```bash
|
||||
cargo install cargo-leptos
|
||||
cargo leptos new --git https://github.com/leptos-rs/start
|
||||
cargo leptos new --git https://github.com/leptos-rs/start-axum
|
||||
cd [your project name]
|
||||
cargo leptos watch
|
||||
```
|
||||
@@ -147,7 +125,7 @@ Yes, I’m sure there are. You can see from the state of our issue tracker over
|
||||
|
||||
This may be the big one: “production ready” implies a certain orientation to a library: that you can basically use it, without any special knowledge of its internals or ability to contribute. Everyone has this at some level in their stack: for example I (@gbj) don’t have the capacity or knowledge to contribute to something like `wasm-bindgen` at this point: I simply rely on it to work.
|
||||
|
||||
There are several people in the community using Leptos right now for internal apps at work, who have also become significant contributors. I think this is the right level of production use for now. There may be missing features that you need, and you may end up building them! But for internal apps, if you’re willing to build and contribute missing pieces along the way, the framework is definitely usable right now.
|
||||
There are several people in the community using Leptos right now for many websites at work, who have also become significant contributors. There may be missing features that you need, and you may end up building them! But, if you're willing to contribute a few missing pieces along the way, the framework is most definitely usable for production applications, especially given the ecosystem of libraries that have sprung up around it.
|
||||
|
||||
### Can I use this for native GUI?
|
||||
|
||||
|
||||
@@ -10,4 +10,4 @@ rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
pin-project-lite = "0.2.15"
|
||||
pin-project-lite = { workspace = true, default-features = true }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "any_spawner"
|
||||
version = "0.3.0-rc3"
|
||||
version = "0.3.0"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -9,25 +9,25 @@ description = "Spawn asynchronous tasks in an executor-independent way."
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
async-executor = { version = "1.13.1", optional = true }
|
||||
futures = "0.3.31"
|
||||
glib = { version = "0.20.6", optional = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { version = "1.41", optional = true, default-features = false, features = [
|
||||
async-executor = { optional = true , workspace = true, default-features = true }
|
||||
futures = { workspace = true, default-features = true }
|
||||
glib = { optional = true , workspace = true, default-features = true }
|
||||
thiserror = { workspace = true , default-features = true }
|
||||
tokio = { optional = true, default-features = false, features = [
|
||||
"rt",
|
||||
] }
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
wasm-bindgen-futures = { version = "0.4.50", optional = true }
|
||||
] , workspace = true }
|
||||
tracing = { optional = true , workspace = true, default-features = true }
|
||||
wasm-bindgen-futures = { optional = true , workspace = true, default-features = true }
|
||||
|
||||
[dev-dependencies]
|
||||
futures-lite = { version = "2.6.0", default-features = false }
|
||||
tokio = { version = "1.41", default-features = false, features = [
|
||||
futures-lite = { default-features = false , workspace = true }
|
||||
tokio = { default-features = false, features = [
|
||||
"rt",
|
||||
"macros",
|
||||
"time",
|
||||
] }
|
||||
wasm-bindgen-test = { version = "0.3.50" }
|
||||
serial_test = "3.2.0"
|
||||
] , workspace = true }
|
||||
wasm-bindgen-test = { workspace = true, default-features = true }
|
||||
serial_test = { workspace = true, default-features = true }
|
||||
|
||||
[features]
|
||||
async-executor = ["dep:async-executor"]
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
name = "benchmarks"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
# std::sync::LazyLock is stabilized in Rust version 1.80.0
|
||||
rust-version = "1.80.0"
|
||||
|
||||
[dependencies]
|
||||
l0410 = { package = "leptos", version = "0.4.10", features = [
|
||||
|
||||
14
cargo-make/check-minimal-versions.toml
Normal file
14
cargo-make/check-minimal-versions.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[tasks.check-minimal-versions]
|
||||
condition = { channels = ["nightly"] }
|
||||
command = "cargo"
|
||||
args = [
|
||||
"all-features",
|
||||
"minimal-versions",
|
||||
"check",
|
||||
"--ignore-private",
|
||||
"--detach-path-deps",
|
||||
"--direct",
|
||||
]
|
||||
install_script = '''
|
||||
cargo install --git https://github.com/sabify/cargo-all-features --branch arbitrary-command-support
|
||||
'''
|
||||
@@ -1,4 +1,8 @@
|
||||
extend = [{ path = "./lint.toml" }, { path = "./test.toml" }]
|
||||
extend = [
|
||||
{ path = "./lint.toml" },
|
||||
{ path = "./test.toml" },
|
||||
{ path = "./check-minimal-versions.toml" },
|
||||
]
|
||||
|
||||
[env]
|
||||
RUSTFLAGS = ""
|
||||
|
||||
@@ -31,7 +31,7 @@ pub const fn const_concat(
|
||||
let mut i = 0;
|
||||
|
||||
// have it iterate over bytes manually, because, again,
|
||||
// no mutable refernces in const fns
|
||||
// no mutable references in const fns
|
||||
while i < x.len() {
|
||||
buffer[position] = x[i];
|
||||
position += 1;
|
||||
@@ -59,7 +59,7 @@ pub const fn const_concat_with_prefix(
|
||||
let mut i = 0;
|
||||
|
||||
// have it iterate over bytes manually, because, again,
|
||||
// no mutable refernces in const fns
|
||||
// no mutable references in const fns
|
||||
while i < x.len() {
|
||||
buffer[position] = x[i];
|
||||
position += 1;
|
||||
@@ -116,7 +116,7 @@ pub const fn const_concat_with_separator(
|
||||
let mut i = 0;
|
||||
|
||||
// have it iterate over bytes manually, because, again,
|
||||
// no mutable refernces in const fns
|
||||
// no mutable references in const fns
|
||||
while i < x.len() {
|
||||
buffer[position] = x[i];
|
||||
position += 1;
|
||||
|
||||
@@ -10,8 +10,8 @@ rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
pin-project-lite = "0.2.16"
|
||||
paste = "1.0.15"
|
||||
pin-project-lite = { workspace = true, default-features = true }
|
||||
paste = { workspace = true, default-features = true }
|
||||
|
||||
[features]
|
||||
default = ["no_std"]
|
||||
|
||||
@@ -19,7 +19,7 @@ leptos_meta = { path = "../../meta" }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_router = { path = "../../router" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
thiserror = "1.0"
|
||||
thiserror = "2.0.12"
|
||||
tokio = { version = "1.39", features = [
|
||||
"rt-multi-thread",
|
||||
"macros",
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
name = "counter_isomorphic"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
# std::sync::LazyLock is stabilized in Rust version 1.80.0
|
||||
rust-version = "1.80.0"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
@@ -23,7 +21,6 @@ leptos = { path = "../../leptos" }
|
||||
leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
leptos_router = { path = "../../router" }
|
||||
log = "0.4.22"
|
||||
once_cell = "1.19"
|
||||
gloo-net = { version = "0.6.0" }
|
||||
wasm-bindgen = "0.2.93"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
||||
@@ -18,7 +18,7 @@ tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
http = { version = "1.1" }
|
||||
thiserror = "1.0"
|
||||
thiserror = "2.0.12"
|
||||
wasm-bindgen = "0.2.93"
|
||||
|
||||
[features]
|
||||
|
||||
@@ -15,7 +15,7 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
log = "0.4.22"
|
||||
console_log = "1.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
thiserror = "1.0"
|
||||
thiserror = "2.0.12"
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = "0.3.18"
|
||||
tracing-subscriber-wasm = "0.1.0"
|
||||
|
||||
@@ -7,7 +7,7 @@ pub fn main() {
|
||||
|
||||
fmt()
|
||||
.with_writer(
|
||||
// To avoide trace events in the browser from showing their
|
||||
// To avoid trace events in the browser from showing their
|
||||
// JS backtrace, which is very annoying, in my opinion
|
||||
MakeConsoleWriter::default()
|
||||
.map_trace_level_to(tracing::Level::DEBUG),
|
||||
|
||||
@@ -6,7 +6,7 @@ use leptos_router::{components::A, hooks::use_params_map};
|
||||
#[component]
|
||||
pub fn Story() -> impl IntoView {
|
||||
let params = use_params_map();
|
||||
let story = Resource::new(
|
||||
let story = Resource::new_blocking(
|
||||
move || params.read().get("id").unwrap_or_default(),
|
||||
move |id| async move {
|
||||
if id.is_empty() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use axum::Router;
|
||||
use axum::{routing::get, Router};
|
||||
use hackernews_axum::{shell, App};
|
||||
use leptos::config::get_configuration;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
@@ -13,6 +13,15 @@ async fn main() {
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route(
|
||||
"/favicon.ico",
|
||||
get(|| async {
|
||||
(
|
||||
[("content-type", "image/x-icon")],
|
||||
include_bytes!("../public/favicon.ico"),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.leptos_routes(&leptos_options, routes, {
|
||||
let leptos_options = leptos_options.clone();
|
||||
move || shell(leptos_options.clone())
|
||||
|
||||
@@ -6,7 +6,7 @@ use leptos_router::{components::A, hooks::use_params_map};
|
||||
#[component]
|
||||
pub fn Story() -> impl IntoView {
|
||||
let params = use_params_map();
|
||||
let story = Resource::new(
|
||||
let story = Resource::new_blocking(
|
||||
move || params.read().get("id").unwrap_or_default(),
|
||||
move |id| async move {
|
||||
if id.is_empty() {
|
||||
|
||||
@@ -26,12 +26,17 @@ pub async fn file_and_error_handler(
|
||||
.map(|h| h.to_str().unwrap_or("none"))
|
||||
.unwrap_or("none")
|
||||
.to_string();
|
||||
let res = get_static_file(uri.clone(), accept_encoding).await.unwrap();
|
||||
let static_result = get_static_file(uri.clone(), accept_encoding).await;
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else {
|
||||
(StatusCode::NOT_FOUND, "Not found.").into_response()
|
||||
match static_result {
|
||||
Ok(res) => {
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else {
|
||||
(StatusCode::NOT_FOUND, "Not found.").into_response()
|
||||
}
|
||||
}
|
||||
Err(e) => e.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use axum::routing::get;
|
||||
pub use axum::Router;
|
||||
use hackernews_islands::*;
|
||||
pub use leptos::config::get_configuration;
|
||||
@@ -25,6 +26,7 @@ async fn main() {
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/favicon.ico", get(fallback::file_and_error_handler))
|
||||
.leptos_routes(&leptos_options, routes, {
|
||||
let leptos_options = leptos_options.clone();
|
||||
move || shell(leptos_options.clone())
|
||||
|
||||
@@ -47,7 +47,7 @@ pub fn Stories() -> impl IntoView {
|
||||
let stories = Resource::new(
|
||||
move || (page(), story_type()),
|
||||
move |(page, story_type)| async move {
|
||||
fetch_stories(category(&story_type), page).await.ok()
|
||||
fetch_stories(story_type, page).await.ok()
|
||||
},
|
||||
);
|
||||
let (pending, set_pending) = signal(false);
|
||||
|
||||
@@ -13,7 +13,7 @@ pub async fn fetch_story(
|
||||
#[component]
|
||||
pub fn Story() -> impl IntoView {
|
||||
let params = use_params_map();
|
||||
let story = Resource::new(
|
||||
let story = Resource::new_blocking(
|
||||
move || params.read().get("id").unwrap_or_default(),
|
||||
move |id| async move {
|
||||
if id.is_empty() {
|
||||
|
||||
@@ -7,7 +7,7 @@ use send_wrapper::SendWrapper;
|
||||
#[component]
|
||||
pub fn Story() -> impl IntoView {
|
||||
let params = use_params_map();
|
||||
let story = Resource::new(
|
||||
let story = Resource::new_blocking(
|
||||
move || params.read().get("id").unwrap_or_default(),
|
||||
move |id| {
|
||||
SendWrapper::new(async move {
|
||||
|
||||
@@ -10,7 +10,11 @@ crate-type = ["cdylib", "rlib"]
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.30"
|
||||
http = "1.1"
|
||||
leptos = { path = "../../leptos", features = ["tracing", "islands"] }
|
||||
leptos = { path = "../../leptos", features = [
|
||||
"tracing",
|
||||
"islands",
|
||||
"islands-router",
|
||||
] }
|
||||
leptos_router = { path = "../../router" }
|
||||
server_fn = { path = "../../server_fn", features = ["serde-lite"] }
|
||||
leptos_axum = { path = "../../integrations/axum", features = [
|
||||
|
||||
90
examples/regression/Cargo.toml
Normal file
90
examples/regression/Cargo.toml
Normal file
@@ -0,0 +1,90 @@
|
||||
[package]
|
||||
name = "regression"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
console_error_panic_hook = "0.1.7"
|
||||
console_log = "1.0"
|
||||
leptos = { path = "../../leptos", features = ["tracing"] }
|
||||
leptos_meta = { path = "../../meta" }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_router = { path = "../../router" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
thiserror = "2.0.12"
|
||||
tokio = { version = "1.39", features = [ "rt-multi-thread", "macros", "time" ], optional = true }
|
||||
wasm-bindgen = "0.2.92"
|
||||
|
||||
[features]
|
||||
hydrate = [
|
||||
"leptos/hydrate",
|
||||
]
|
||||
ssr = [
|
||||
"dep:axum",
|
||||
"dep:tokio",
|
||||
"leptos/ssr",
|
||||
"leptos_meta/ssr",
|
||||
"dep:leptos_axum",
|
||||
"leptos_router/ssr",
|
||||
]
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
|
||||
[profile.wasm-release]
|
||||
inherits = "release"
|
||||
opt-level = 'z'
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["axum", "tower", "tower-http", "tokio", "sqlx", "leptos_axum"]
|
||||
skip_feature_sets = [["ssr", "hydrate"]]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
output-name = "regression"
|
||||
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
|
||||
site-root = "target/site"
|
||||
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
|
||||
# Defaults to pkg
|
||||
site-pkg-dir = "pkg"
|
||||
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
|
||||
site-addr = "127.0.0.1:3000"
|
||||
# The port to use for automatic reload monitoring
|
||||
reload-port = 3001
|
||||
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
||||
# [Windows] for non-WSL use "npx.cmd playwright test"
|
||||
# This binary name can be checked in Powershell with Get-Command npx
|
||||
end2end-cmd = "cargo make test-ui"
|
||||
end2end-dir = "e2e"
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
browserquery = "defaults"
|
||||
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
|
||||
watch = false
|
||||
# The environment Leptos will run in, usually either "DEV" or "PROD"
|
||||
env = "DEV"
|
||||
# The features to use when compiling the bin target
|
||||
#
|
||||
# Optional. Can be over-ridden with the command line parameter --bin-features
|
||||
bin-features = ["ssr"]
|
||||
|
||||
# If the --no-default-features flag should be used when compiling the bin target
|
||||
#
|
||||
# Optional. Defaults to false.
|
||||
bin-default-features = false
|
||||
|
||||
# The features to use when compiling the lib target
|
||||
#
|
||||
# Optional. Can be over-ridden with the command line parameter --lib-features
|
||||
lib-features = ["hydrate"]
|
||||
|
||||
# If the --no-default-features flag should be used when compiling the lib target
|
||||
#
|
||||
# Optional. Defaults to false.
|
||||
lib-default-features = false
|
||||
21
examples/regression/LICENSE
Normal file
21
examples/regression/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Leptos
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
8
examples/regression/Makefile.toml
Normal file
8
examples/regression/Makefile.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/cargo-leptos-webdriver-test.toml" },
|
||||
]
|
||||
|
||||
[env]
|
||||
|
||||
CLIENT_PROCESS_NAME = "regression"
|
||||
8
examples/regression/README.md
Normal file
8
examples/regression/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Regression Tests
|
||||
|
||||
This example functions as a catch-all for all current and future regression
|
||||
test cases that typically happens at integration.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Run `cargo leptos watch` to run this example.
|
||||
BIN
examples/regression/assets/favicon.ico
Normal file
BIN
examples/regression/assets/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
18
examples/regression/e2e/Cargo.toml
Normal file
18
examples/regression/e2e/Cargo.toml
Normal file
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "regression_e2e"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "1.0"
|
||||
async-trait = "0.1.81"
|
||||
cucumber = "0.21.1"
|
||||
fantoccini = "0.21.1"
|
||||
pretty_assertions = "1.4"
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1.39", features = ["macros", "rt-multi-thread", "time"] }
|
||||
url = "2.5"
|
||||
|
||||
[[test]]
|
||||
name = "app_suite"
|
||||
harness = false # Allow Cucumber to print output instead of libtest
|
||||
20
examples/regression/e2e/Makefile.toml
Normal file
20
examples/regression/e2e/Makefile.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
extend = { path = "../../cargo-make/main.toml" }
|
||||
|
||||
[tasks.test]
|
||||
env = { RUN_AUTOMATICALLY = false }
|
||||
condition = { env_true = ["RUN_AUTOMATICALLY"] }
|
||||
|
||||
[tasks.ci]
|
||||
|
||||
[tasks.test-ui]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"test",
|
||||
"--test",
|
||||
"app_suite",
|
||||
"--",
|
||||
"--retry",
|
||||
"2",
|
||||
"--fail-fast",
|
||||
"${@}",
|
||||
]
|
||||
34
examples/regression/e2e/README.md
Normal file
34
examples/regression/e2e/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# E2E Testing
|
||||
|
||||
This example demonstrates e2e testing with Rust using executable requirements.
|
||||
|
||||
## Testing Stack
|
||||
|
||||
| | Role | Description |
|
||||
|---|---|---|
|
||||
| [Cucumber](https://github.com/cucumber-rs/cucumber/tree/main) | Test Runner | Run [Gherkin](https://cucumber.io/docs/gherkin/reference/) specifications as Rust tests |
|
||||
| [Fantoccini](https://github.com/jonhoo/fantoccini/tree/main) | Browser Client | Interact with web pages through WebDriver |
|
||||
| [Cargo Leptos](https://github.com/leptos-rs/cargo-leptos) | Build Tool | Compile example and start the server and end-2-end tests |
|
||||
| [chromedriver](https://chromedriver.chromium.org/downloads) | WebDriver | Provide WebDriver for Chrome |
|
||||
|
||||
## Testing Organization
|
||||
|
||||
Testing is organized around what a user can do and see/not see. Test scenarios are grouped by the **user action** and the **object** of that action. This makes it easier to locate and reason about requirements.
|
||||
|
||||
Here is a brief overview of how things fit together.
|
||||
|
||||
```bash
|
||||
features
|
||||
└── {action}_{object}.feature # Specify test scenarios
|
||||
tests
|
||||
├── fixtures
|
||||
│ ├── action.rs # Perform a user action (click, type, etc.)
|
||||
│ ├── check.rs # Assert what a user can see/not see
|
||||
│ ├── find.rs # Query page elements
|
||||
│ ├── mod.rs
|
||||
│ └── world
|
||||
│ ├── action_steps.rs # Map Gherkin steps to user actions
|
||||
│ ├── check_steps.rs # Map Gherkin steps to user expectations
|
||||
│ └── mod.rs
|
||||
└── app_suite.rs # Test main
|
||||
```
|
||||
20
examples/regression/e2e/features/issue_4088.feature
Normal file
20
examples/regression/e2e/features/issue_4088.feature
Normal file
@@ -0,0 +1,20 @@
|
||||
@check_issue_4088
|
||||
Feature: Check that issue 4088 does not reappear
|
||||
|
||||
Scenario: I can see the navbar
|
||||
Given I see the app
|
||||
And I can access regression test 4088
|
||||
Then I see the navbar
|
||||
|
||||
Scenario: The user info is shared via context
|
||||
Given I see the app
|
||||
And I can access regression test 4088
|
||||
When I select the link Class 1
|
||||
Then I see the result is the string Assignments for team of user with id 42
|
||||
|
||||
Scenario: The user info is shared via context
|
||||
Given I see the app
|
||||
And I can access regression test 4088
|
||||
When I select the link Class 1
|
||||
When I refresh the browser
|
||||
Then I see the result is the string Assignments for team of user with id 42
|
||||
8
examples/regression/e2e/features/pr_4015.feature
Normal file
8
examples/regression/e2e/features/pr_4015.feature
Normal file
@@ -0,0 +1,8 @@
|
||||
@check_pr_4015
|
||||
Feature: Check that PR 4015 does not regress
|
||||
|
||||
Scenario: The correct text appears
|
||||
Given I see the app
|
||||
And I can access regression test 4015
|
||||
Then I see the result is the string Some(42)
|
||||
|
||||
48
examples/regression/e2e/features/pr_4091.feature
Normal file
48
examples/regression/e2e/features/pr_4091.feature
Normal file
@@ -0,0 +1,48 @@
|
||||
@check_pr_4091
|
||||
Feature: Regression from pull request 4091
|
||||
|
||||
Scenario: Signal for testing should work
|
||||
Given I see the app
|
||||
And I can access regression test 4091
|
||||
When I select the link test1
|
||||
Then I see the result is the string Test1
|
||||
|
||||
Scenario: The result returns to empty due to on_cleanup
|
||||
Given I see the app
|
||||
And I can access regression test 4091
|
||||
When I select the following links
|
||||
| test1 |
|
||||
| 4091 Home |
|
||||
Then I see the result is empty
|
||||
|
||||
Scenario: The result does not accumulate due to on_cleanup
|
||||
Given I see the app
|
||||
And I can access regression test 4091
|
||||
When I select the following links
|
||||
| test1 |
|
||||
| 4091 Home |
|
||||
| test1 |
|
||||
| 4091 Home |
|
||||
Then I see the result is empty
|
||||
|
||||
Scenario: I can see the navbar
|
||||
Given I see the app
|
||||
And I can access regression test 4091
|
||||
Then I see the navbar
|
||||
|
||||
Scenario: If I navigate to home and back, I can still see the navbar
|
||||
Given I see the app
|
||||
And I can access regression test 4091
|
||||
When I select the following links
|
||||
| Home |
|
||||
| 4091 |
|
||||
Then I see the navbar
|
||||
|
||||
Scenario: The signal is not disposed too early
|
||||
Given I see the app
|
||||
And I can access regression test 4091
|
||||
When I select the following links
|
||||
| test1 |
|
||||
| Home |
|
||||
| 4091 |
|
||||
Then I see the navbar
|
||||
30
examples/regression/e2e/tests/app_suite.rs
Normal file
30
examples/regression/e2e/tests/app_suite.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
mod fixtures;
|
||||
|
||||
use anyhow::Result;
|
||||
use cucumber::World;
|
||||
use fixtures::world::AppWorld;
|
||||
use std::{ffi::OsStr, fs::read_dir};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Normally the below is done, but it's now gotten to the point of
|
||||
// having a sufficient number of tests where the resource contention
|
||||
// of the concurrently running browsers will cause failures on CI.
|
||||
// AppWorld::cucumber()
|
||||
// .fail_on_skipped()
|
||||
// .run_and_exit("./features")
|
||||
// .await;
|
||||
|
||||
// Mitigate the issue by manually stepping through each feature,
|
||||
// rather than letting cucumber glob them and dispatch all at once.
|
||||
for entry in read_dir("./features")? {
|
||||
let path = entry?.path();
|
||||
if path.extension() == Some(OsStr::new("feature")) {
|
||||
AppWorld::cucumber()
|
||||
.fail_on_skipped()
|
||||
.run_and_exit(path)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
17
examples/regression/e2e/tests/fixtures/action.rs
vendored
Normal file
17
examples/regression/e2e/tests/fixtures/action.rs
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
use super::{find, world::HOST};
|
||||
use anyhow::Result;
|
||||
use fantoccini::Client;
|
||||
use std::result::Result::Ok;
|
||||
|
||||
pub async fn goto_path(client: &Client, path: &str) -> Result<()> {
|
||||
let url = format!("{}{}", HOST, path);
|
||||
client.goto(&url).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn click_link(client: &Client, text: &str) -> Result<()> {
|
||||
let link = find::link_with_text(&client, &text).await?;
|
||||
link.click().await?;
|
||||
Ok(())
|
||||
}
|
||||
20
examples/regression/e2e/tests/fixtures/check.rs
vendored
Normal file
20
examples/regression/e2e/tests/fixtures/check.rs
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
use crate::fixtures::find;
|
||||
use anyhow::{Ok, Result};
|
||||
use fantoccini::Client;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
pub async fn result_text_is(
|
||||
client: &Client,
|
||||
expected_text: &str,
|
||||
) -> Result<()> {
|
||||
let actual = find::text_at_id(client, "result").await?;
|
||||
assert_eq!(&actual, expected_text);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn element_exists(client: &Client, id: &str) -> Result<()> {
|
||||
find::element_by_id(client, id)
|
||||
.await
|
||||
.expect(&format!("could not find element with id `{id}`"));
|
||||
Ok(())
|
||||
}
|
||||
23
examples/regression/e2e/tests/fixtures/find.rs
vendored
Normal file
23
examples/regression/e2e/tests/fixtures/find.rs
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
use anyhow::{Ok, Result};
|
||||
use fantoccini::{elements::Element, Client, Locator};
|
||||
|
||||
pub async fn text_at_id(client: &Client, id: &str) -> Result<String> {
|
||||
let element = element_by_id(client, id)
|
||||
.await
|
||||
.expect(format!("no such element with id `{}`", id).as_str());
|
||||
let text = element.text().await?;
|
||||
Ok(text)
|
||||
}
|
||||
|
||||
pub async fn link_with_text(client: &Client, text: &str) -> Result<Element> {
|
||||
let link = client
|
||||
.wait()
|
||||
.for_element(Locator::LinkText(text))
|
||||
.await
|
||||
.expect(format!("Link not found by `{}`", text).as_str());
|
||||
Ok(link)
|
||||
}
|
||||
|
||||
pub async fn element_by_id(client: &Client, id: &str) -> Result<Element> {
|
||||
Ok(client.wait().for_element(Locator::Id(id)).await?)
|
||||
}
|
||||
4
examples/regression/e2e/tests/fixtures/mod.rs
vendored
Normal file
4
examples/regression/e2e/tests/fixtures/mod.rs
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod action;
|
||||
pub mod check;
|
||||
pub mod find;
|
||||
pub mod world;
|
||||
47
examples/regression/e2e/tests/fixtures/world/action_steps.rs
vendored
Normal file
47
examples/regression/e2e/tests/fixtures/world/action_steps.rs
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
use crate::fixtures::{action, world::AppWorld};
|
||||
use anyhow::{Ok, Result};
|
||||
use cucumber::{gherkin::Step, given, when};
|
||||
|
||||
#[given("I see the app")]
|
||||
#[when("I open the app")]
|
||||
async fn i_open_the_app(world: &mut AppWorld) -> Result<()> {
|
||||
let client = &world.client;
|
||||
action::goto_path(client, "").await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[given(regex = "^I can access regression test (.*)$")]
|
||||
#[when(regex = "^I select the link (.*)$")]
|
||||
async fn i_select_the_link(world: &mut AppWorld, text: String) -> Result<()> {
|
||||
let client = &world.client;
|
||||
action::click_link(client, &text).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[given(expr = "I select the following links")]
|
||||
#[when(expr = "I select the following links")]
|
||||
async fn i_select_the_following_links(
|
||||
world: &mut AppWorld,
|
||||
step: &Step,
|
||||
) -> Result<()> {
|
||||
let client = &world.client;
|
||||
|
||||
if let Some(table) = step.table.as_ref() {
|
||||
for row in table.rows.iter() {
|
||||
action::click_link(client, &row[0]).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[given(regex = "^I (refresh|reload) the (browser|page)$")]
|
||||
#[when(regex = "^I (refresh|reload) the (browser|page)$")]
|
||||
async fn i_refresh_the_browser(world: &mut AppWorld) -> Result<()> {
|
||||
let client = &world.client;
|
||||
client.refresh().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
27
examples/regression/e2e/tests/fixtures/world/check_steps.rs
vendored
Normal file
27
examples/regression/e2e/tests/fixtures/world/check_steps.rs
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
use crate::fixtures::{check, world::AppWorld};
|
||||
use anyhow::{Ok, Result};
|
||||
use cucumber::then;
|
||||
|
||||
#[then(regex = r"^I see the result is empty$")]
|
||||
async fn i_see_the_result_is_empty(world: &mut AppWorld) -> Result<()> {
|
||||
let client = &world.client;
|
||||
check::result_text_is(client, "").await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[then(regex = r"^I see the result is the string (.*)$")]
|
||||
async fn i_see_the_result_is_the_string(
|
||||
world: &mut AppWorld,
|
||||
text: String,
|
||||
) -> Result<()> {
|
||||
let client = &world.client;
|
||||
check::result_text_is(client, &text).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[then(regex = r"^I see the navbar$")]
|
||||
async fn i_see_the_navbar(world: &mut AppWorld) -> Result<()> {
|
||||
let client = &world.client;
|
||||
check::element_exists(client, "nav").await?;
|
||||
Ok(())
|
||||
}
|
||||
39
examples/regression/e2e/tests/fixtures/world/mod.rs
vendored
Normal file
39
examples/regression/e2e/tests/fixtures/world/mod.rs
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
pub mod action_steps;
|
||||
pub mod check_steps;
|
||||
|
||||
use anyhow::Result;
|
||||
use cucumber::World;
|
||||
use fantoccini::{
|
||||
error::NewSessionError, wd::Capabilities, Client, ClientBuilder,
|
||||
};
|
||||
|
||||
pub const HOST: &str = "http://127.0.0.1:3000";
|
||||
|
||||
#[derive(Debug, World)]
|
||||
#[world(init = Self::new)]
|
||||
pub struct AppWorld {
|
||||
pub client: Client,
|
||||
}
|
||||
|
||||
impl AppWorld {
|
||||
async fn new() -> Result<Self, anyhow::Error> {
|
||||
let webdriver_client = build_client().await?;
|
||||
|
||||
Ok(Self {
|
||||
client: webdriver_client,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn build_client() -> Result<Client, NewSessionError> {
|
||||
let mut cap = Capabilities::new();
|
||||
let arg = serde_json::from_str("{\"args\": [\"-headless\"]}").unwrap();
|
||||
cap.insert("goog:chromeOptions".to_string(), arg);
|
||||
|
||||
let client = ClientBuilder::native()
|
||||
.capabilities(cap)
|
||||
.connect("http://localhost:4444")
|
||||
.await?;
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
65
examples/regression/src/app.rs
Normal file
65
examples/regression/src/app.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
use crate::{issue_4088::Routes4088, pr_4015::Routes4015, pr_4091::Routes4091};
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::{MetaTags, *};
|
||||
use leptos_router::{
|
||||
components::{Route, Router, Routes},
|
||||
path,
|
||||
};
|
||||
|
||||
pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
view! {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<AutoReload options=options.clone()/>
|
||||
<HydrationScripts options/>
|
||||
<MetaTags/>
|
||||
</head>
|
||||
<body>
|
||||
<App/>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
provide_meta_context();
|
||||
let fallback = || view! { "Page not found." }.into_view();
|
||||
view! {
|
||||
<Stylesheet id="leptos" href="/pkg/regression.css"/>
|
||||
<Router>
|
||||
<main>
|
||||
<Routes fallback>
|
||||
<Route path=path!("") view=HomePage/>
|
||||
<Routes4091/>
|
||||
<Routes4015/>
|
||||
<Routes4088/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[server]
|
||||
async fn server_call() -> Result<(), ServerFnError> {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn HomePage() -> impl IntoView {
|
||||
view! {
|
||||
<Title text="Regression Tests"/>
|
||||
<h1>"Listing of regression tests"</h1>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="/4091/">"4091"</a></li>
|
||||
<li><a href="/4015/">"4015"</a></li>
|
||||
<li><a href="/4088/">"4088"</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
}
|
||||
}
|
||||
119
examples/regression/src/issue_4088.rs
Normal file
119
examples/regression/src/issue_4088.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
use leptos::{either::Either, prelude::*};
|
||||
#[allow(unused_imports)]
|
||||
use leptos_router::{
|
||||
components::{Outlet, ParentRoute, Redirect, Route},
|
||||
path, MatchNestedRoutes, NavigateOptions,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[component]
|
||||
pub fn Routes4088() -> impl MatchNestedRoutes + Clone {
|
||||
view! {
|
||||
<ParentRoute path=path!("4088") view=|| view!{ <LoggedIn/> }>
|
||||
<ParentRoute path=path!("") view=||view!{<AssignmentsSelector/>}>
|
||||
<Route path=path!("/:team_id") view=||view!{<AssignmentsForTeam/>} />
|
||||
<Route path=path!("") view=||view!{ <p>No class selected</p> }/>
|
||||
</ParentRoute>
|
||||
</ParentRoute>
|
||||
}
|
||||
.into_inner()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct UserInfo {
|
||||
pub id: usize,
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn get_user_info() -> Result<Option<UserInfo>, ServerFnError> {
|
||||
Ok(Some(UserInfo { id: 42 }))
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn LoggedIn() -> impl IntoView {
|
||||
let user_info_resource =
|
||||
Resource::new(|| (), move |_| async { get_user_info().await });
|
||||
|
||||
view! {
|
||||
|
||||
<Transition fallback=move || view!{
|
||||
"loading"
|
||||
}
|
||||
>
|
||||
{move || {
|
||||
user_info_resource.get()
|
||||
.map(|a|
|
||||
match a {
|
||||
Ok(Some(a)) => Either::Left(view! {
|
||||
<LoggedInContent user_info={a} />
|
||||
}),
|
||||
_ => Either::Right(view!{
|
||||
<Redirect path="/not_logged_in"/>
|
||||
})
|
||||
})
|
||||
}}
|
||||
</Transition>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
/// Component which provides UserInfo and renders it's child
|
||||
/// Can also contain some code to check for specific situations (e.g. privacy policies accepted or not? redirect if needed...)
|
||||
pub fn LoggedInContent(user_info: UserInfo) -> impl IntoView {
|
||||
provide_context(user_info.clone());
|
||||
|
||||
if user_info.id == 42 {
|
||||
Either::Left(Outlet())
|
||||
} else {
|
||||
Either::Right(
|
||||
view! { <Redirect path="/somewhere" options={NavigateOptions::default()}/> },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
/// This component also uses Outlet (so nested Outlet)
|
||||
fn AssignmentsSelector() -> impl IntoView {
|
||||
let user_info = use_context::<UserInfo>().expect("user info not provided");
|
||||
|
||||
view! {
|
||||
<p>"Assignments for user with ID: "{user_info.id}</p>
|
||||
<ul id="nav">
|
||||
<li><a href="/4088/1">"Class 1"</a></li>
|
||||
<li><a href="/4088/2">"Class 2"</a></li>
|
||||
<li><a href="/4088/3">"Class 3"</a></li>
|
||||
</ul>
|
||||
|
||||
<Outlet />
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn AssignmentsForTeam() -> impl IntoView {
|
||||
// THIS FAILS -> Because of the nested outlet in LoggedInContent > AssignmentsSelector?
|
||||
// It did not fail when LoggedIn did not use a resource and transition (but a hardcoded UserInfo in the component)
|
||||
let user_info = use_context::<UserInfo>().expect("user info not provided");
|
||||
|
||||
let items = vec!["Assignment 1", "Assignment 2", "Assignment 3"];
|
||||
view! {
|
||||
<p id="result">"Assignments for team of user with id " {user_info.id}</p>
|
||||
<ul>
|
||||
{
|
||||
items.into_iter().map(|item| {
|
||||
view! {
|
||||
<Assignment name=item.to_string() />
|
||||
}
|
||||
}).collect_view()
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Assignment(name: String) -> impl IntoView {
|
||||
let user_info = use_context::<UserInfo>().expect("user info not provided");
|
||||
|
||||
view! {
|
||||
<li>{name}" "{user_info.id}</li>
|
||||
}
|
||||
}
|
||||
12
examples/regression/src/lib.rs
Normal file
12
examples/regression/src/lib.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
pub mod app;
|
||||
mod issue_4088;
|
||||
mod pr_4015;
|
||||
mod pr_4091;
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
use app::*;
|
||||
console_error_panic_hook::set_once();
|
||||
leptos::mount::hydrate_body(App);
|
||||
}
|
||||
37
examples/regression/src/main.rs
Normal file
37
examples/regression/src/main.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use axum::Router;
|
||||
use leptos::prelude::*;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use regression::app::{shell, App};
|
||||
|
||||
let conf = get_configuration(None).unwrap();
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
let leptos_options = conf.leptos_options;
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(App);
|
||||
|
||||
let app = Router::new()
|
||||
.leptos_routes(&leptos_options, routes, {
|
||||
let leptos_options = leptos_options.clone();
|
||||
move || shell(leptos_options.clone())
|
||||
})
|
||||
.fallback(leptos_axum::file_and_error_handler(shell))
|
||||
.with_state(leptos_options);
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
println!("listening on http://{}", &addr);
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn main() {
|
||||
// no client-side main function
|
||||
// unless we want this to work with e.g., Trunk for pure client-side testing
|
||||
// see lib.rs for hydration function instead
|
||||
}
|
||||
29
examples/regression/src/pr_4015.rs
Normal file
29
examples/regression/src/pr_4015.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use leptos::{context::Provider, prelude::*};
|
||||
use leptos_router::{
|
||||
components::{ParentRoute, Route},
|
||||
nested_router::Outlet,
|
||||
path,
|
||||
};
|
||||
|
||||
#[component]
|
||||
pub fn Routes4015() -> impl leptos_router::MatchNestedRoutes + Clone {
|
||||
view! {
|
||||
<ParentRoute path=path!("4015") view=|| view! {
|
||||
<Provider value=42i32>
|
||||
<Outlet/>
|
||||
</Provider>
|
||||
}>
|
||||
<Route path=path!("") view=Child/>
|
||||
</ParentRoute>
|
||||
}
|
||||
.into_inner()
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Child() -> impl IntoView {
|
||||
let value = use_context::<i32>();
|
||||
|
||||
view! {
|
||||
<p id="result">{format!("{value:?}")}</p>
|
||||
}
|
||||
}
|
||||
68
examples/regression/src/pr_4091.rs
Normal file
68
examples/regression/src/pr_4091.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
use leptos::{context::Provider, prelude::*};
|
||||
use leptos_router::{
|
||||
components::{ParentRoute, Route, A},
|
||||
nested_router::Outlet,
|
||||
path,
|
||||
};
|
||||
|
||||
// FIXME This should be a set rather than a naive vec for push and pop, as
|
||||
// it may be possible for unexpected token be popped/pushed on multi-level
|
||||
// navigation. For basic naive tests it should be Fine(TM).
|
||||
#[derive(Clone)]
|
||||
struct Expectations(Vec<&'static str>);
|
||||
|
||||
#[component]
|
||||
pub fn Routes4091() -> impl leptos_router::MatchNestedRoutes + Clone {
|
||||
view! {
|
||||
<ParentRoute path=path!("4091") view=Container>
|
||||
<Route path=path!("") view=Root/>
|
||||
<Route path=path!("test1") view=Test1/>
|
||||
</ParentRoute>
|
||||
}
|
||||
.into_inner()
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Container() -> impl IntoView {
|
||||
let rw_signal = RwSignal::new(Expectations(Vec::new()));
|
||||
provide_context(rw_signal);
|
||||
|
||||
view! {
|
||||
<nav id="nav">
|
||||
<ul>
|
||||
<li><A href="/">"Home"</A></li>
|
||||
<li><A href="./">"4091 Home"</A></li>
|
||||
<li><A href="test1">"test1"</A></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div id="result">{move || {
|
||||
rw_signal.with(|ex| ex.0.iter().fold(String::new(), |a, b| a + b + " "))
|
||||
}}</div>
|
||||
<Provider value=rw_signal>
|
||||
<Outlet/>
|
||||
</Provider>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Root() -> impl IntoView {
|
||||
view! {
|
||||
<div>"This is Root"</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Test1() -> impl IntoView {
|
||||
let signal = expect_context::<RwSignal<Expectations>>();
|
||||
|
||||
on_cleanup(move || {
|
||||
signal.update(|ex| {
|
||||
ex.0.pop();
|
||||
});
|
||||
});
|
||||
|
||||
view! {
|
||||
{move || signal.update(|ex| ex.0.push("Test1"))}
|
||||
<div>"This is Test1"</div>
|
||||
}
|
||||
}
|
||||
3
examples/regression/style/main.scss
Normal file
3
examples/regression/style/main.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
@@ -29,7 +29,7 @@ tower-http = { version = "0.6.2", features = [
|
||||
"trace",
|
||||
], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
thiserror = "2.0.11"
|
||||
thiserror = "2.0.12"
|
||||
wasm-bindgen = "0.2.93"
|
||||
serde_toml = "0.0.1"
|
||||
toml = "0.8.19"
|
||||
@@ -38,7 +38,6 @@ strum = { version = "0.27.1", features = ["strum_macros", "derive"] }
|
||||
notify = { version = "8.0", optional = true }
|
||||
pin-project-lite = "0.2.14"
|
||||
dashmap = { version = "6.0", optional = true }
|
||||
once_cell = { version = "1.19", optional = true }
|
||||
async-broadcast = { version = "0.7.1", optional = true }
|
||||
bytecheck = "0.8.0"
|
||||
rkyv = { version = "0.8.8" }
|
||||
@@ -54,7 +53,6 @@ ssr = [
|
||||
"dep:leptos_axum",
|
||||
"dep:notify",
|
||||
"dep:dashmap",
|
||||
"dep:once_cell",
|
||||
"dep:async-broadcast",
|
||||
]
|
||||
|
||||
|
||||
@@ -424,7 +424,7 @@ pub fn FileUploadWithProgress() -> impl IntoView {
|
||||
use async_broadcast::{broadcast, Receiver, Sender};
|
||||
use dashmap::DashMap;
|
||||
use futures::Stream;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
struct File {
|
||||
total: usize,
|
||||
@@ -432,7 +432,8 @@ pub fn FileUploadWithProgress() -> impl IntoView {
|
||||
rx: Receiver<usize>,
|
||||
}
|
||||
|
||||
static FILES: Lazy<DashMap<String, File>> = Lazy::new(DashMap::new);
|
||||
static FILES: LazyLock<DashMap<String, File>> =
|
||||
LazyLock::new(DashMap::new);
|
||||
|
||||
pub async fn add_chunk(filename: &str, len: usize) {
|
||||
println!("[{filename}]\tadding {len}");
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
name = "ssr_modes"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
# std::sync::LazyLock is stabilized in Rust version 1.80.0
|
||||
rust-version = "1.80.0"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
@@ -19,7 +17,7 @@ leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
leptos_router = { path = "../../router" }
|
||||
log = "0.4.22"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
thiserror = "1.0"
|
||||
thiserror = "2.0.12"
|
||||
tokio = { version = "1.39", features = ["time"] }
|
||||
wasm-bindgen = "0.2.93"
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
name = "ssr_modes_axum"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
# std::sync::LazyLock is stabilized in Rust version 1.80.0
|
||||
rust-version = "1.80.0"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
@@ -19,7 +17,7 @@ leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_router = { path = "../../router" }
|
||||
log = "0.4.22"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
thiserror = "1.0"
|
||||
thiserror = "2.0.12"
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
|
||||
@@ -17,7 +17,7 @@ leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_router = { path = "../../router" }
|
||||
log = "0.4.22"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
thiserror = "1.0"
|
||||
thiserror = "2.0.12"
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Leptos Counter Example
|
||||
# Stores Example
|
||||
|
||||
This example creates a simple counter in a client side rendered app with Rust and WASM!
|
||||
This example shows how to use reactive stores, by building a client-side rendered TODO application.
|
||||
|
||||
## Getting Started
|
||||
|
||||
|
||||
@@ -159,7 +159,7 @@ fn TodoRow(
|
||||
|
||||
view! {
|
||||
<li style:text-decoration=move || {
|
||||
status.done().then_some("line-through").unwrap_or_default()
|
||||
if status.done() { "line-through" } else { Default::default() }
|
||||
}>
|
||||
|
||||
<p
|
||||
|
||||
@@ -52,7 +52,7 @@ Feature: Using instrumented counters to test regression from #3502.
|
||||
| list_items | 1 |
|
||||
| get_item | 1 |
|
||||
| inspect_item_root | 0 |
|
||||
| inspect_item_field | 4 |
|
||||
| inspect_item_field | 3 |
|
||||
|
||||
Scenario: Follow paths ordinarily down to a target
|
||||
Given I select the following links
|
||||
|
||||
@@ -477,6 +477,8 @@ fn ItemInspect() -> impl IntoView {
|
||||
move || params.get().map(|p| p.path),
|
||||
move |p| async move {
|
||||
// leptos::logging::log!("res_inspect: res_overview.await");
|
||||
// Note: this resource is untracked here, though `params` changing
|
||||
// will nonetheless results in the "expected" tracked updates.
|
||||
let overview = res_overview.await;
|
||||
// leptos::logging::log!("res_inspect: resolved res_overview.await");
|
||||
// let result =
|
||||
@@ -561,7 +563,7 @@ fn ShowCounters() -> impl IntoView {
|
||||
//
|
||||
// However, upon `Reset Counters`, the mode from which the reset
|
||||
// was issued will result in the rendering be reflected as such, so
|
||||
// if the intial state was SSR, resetting under CSR will result in
|
||||
// if the initial state was SSR, resetting under CSR will result in
|
||||
// the CSR counters be rendered after. However for the intents and
|
||||
// purpose for the testing only the CSR is cared for.
|
||||
//
|
||||
|
||||
@@ -20,7 +20,7 @@ tokio = { version = "1.39", features = [
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
wasm-bindgen = "0.2.93"
|
||||
thiserror = "1.0"
|
||||
thiserror = "2.0.12"
|
||||
tracing = { version = "0.1.40", optional = true }
|
||||
http = "1.1"
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
log = "0.4.22"
|
||||
simple_logger = "5.0"
|
||||
gloo = { git = "https://github.com/rustwasm/gloo" }
|
||||
sqlx = { version = "0.8.0", features = [
|
||||
sqlx = { version = "0.8.6", features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
], optional = true }
|
||||
@@ -44,12 +44,12 @@ denylist = ["actix-files", "actix-web", "leptos_actix", "sqlx"]
|
||||
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 = "todo_app_sqlite"
|
||||
# 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"
|
||||
|
||||
@@ -20,11 +20,11 @@ axum = { version = "0.8.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
sqlx = { version = "0.8.0", features = [
|
||||
sqlx = { version = "0.8.6", features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
], optional = true }
|
||||
thiserror = "1.0"
|
||||
thiserror = "2.0.12"
|
||||
wasm-bindgen = "0.2.93"
|
||||
|
||||
[features]
|
||||
|
||||
@@ -20,11 +20,11 @@ tower = { version = "0.5.1", features = ["util"], optional = true }
|
||||
tower-http = { version = "0.6.1", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
http = { version = "1.1" }
|
||||
sqlx = { version = "0.8.0", features = [
|
||||
sqlx = { version = "0.8.6", features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
], optional = true }
|
||||
thiserror = "2.0"
|
||||
thiserror = "2.0.12"
|
||||
wasm-bindgen = "0.2.93"
|
||||
|
||||
[features]
|
||||
|
||||
@@ -17,7 +17,7 @@ simple_logger = "5.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
thiserror = "2.0"
|
||||
thiserror = "2.0.12"
|
||||
wasm-bindgen = "0.2.100"
|
||||
|
||||
[features]
|
||||
|
||||
10
examples/websocket/e2e/tests/fixtures/action.rs
vendored
10
examples/websocket/e2e/tests/fixtures/action.rs
vendored
@@ -10,19 +10,9 @@ pub async fn goto_path(client: &Client, path: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn add_text(client: &Client, text: &str) -> Result<String> {
|
||||
fill_input(client, text).await?;
|
||||
get_label(client).await
|
||||
}
|
||||
|
||||
pub async fn fill_input(client: &Client, text: &str) -> Result<()> {
|
||||
let textbox = find::input(client).await;
|
||||
textbox.send_keys(text).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_label(client: &Client) -> Result<String> {
|
||||
let label = find::label(client).await;
|
||||
Ok(label.text().await?)
|
||||
}
|
||||
|
||||
10
examples/websocket/e2e/tests/fixtures/find.rs
vendored
10
examples/websocket/e2e/tests/fixtures/find.rs
vendored
@@ -9,13 +9,3 @@ pub async fn input(client: &Client) -> Element {
|
||||
|
||||
textbox
|
||||
}
|
||||
|
||||
pub async fn label(client: &Client) -> Element {
|
||||
let label = client
|
||||
.wait()
|
||||
.for_element(Locator::Css("p"))
|
||||
.await
|
||||
.expect("");
|
||||
|
||||
label
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ async fn i_open_the_app(world: &mut AppWorld) -> Result<()> {
|
||||
#[given(regex = "^I add a text as (.*)$")]
|
||||
async fn i_add_a_text(world: &mut AppWorld, text: String) -> Result<()> {
|
||||
let client = &world.client;
|
||||
action::add_text(client, text.as_str()).await?;
|
||||
action::fill_input(client, text.as_str()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ async fn i_see_the_label_of_the_input_is(
|
||||
world: &mut AppWorld,
|
||||
text: String,
|
||||
) -> Result<()> {
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
let client = &world.client;
|
||||
check::text_on_element(client, "p", &text).await?;
|
||||
|
||||
|
||||
@@ -12,12 +12,11 @@ edition.workspace = true
|
||||
[dependencies]
|
||||
throw_error = { workspace = true }
|
||||
or_poisoned = { workspace = true }
|
||||
futures = "0.3.31"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
wasm-bindgen = { workspace = true, optional = true }
|
||||
js-sys = { version = "0.3.74", optional = true }
|
||||
once_cell = "1.20"
|
||||
pin-project-lite = "0.2.15"
|
||||
futures = { workspace = true, default-features = true }
|
||||
serde = { features = ["derive"] , workspace = true, default-features = true }
|
||||
wasm-bindgen = { workspace = true, optional = true , default-features = true }
|
||||
js-sys = { optional = true , workspace = true, default-features = true }
|
||||
pin-project-lite = { workspace = true, default-features = true }
|
||||
|
||||
[features]
|
||||
browser = ["dep:wasm-bindgen", "dep:js-sys"]
|
||||
|
||||
@@ -7,10 +7,12 @@ use super::{SerializedDataId, SharedContext};
|
||||
use crate::{PinnedFuture, PinnedStream};
|
||||
use core::fmt::Debug;
|
||||
use js_sys::Array;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::{
|
||||
fmt::Display,
|
||||
sync::atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||
LazyLock,
|
||||
},
|
||||
};
|
||||
use throw_error::{Error, ErrorId};
|
||||
use wasm_bindgen::{prelude::wasm_bindgen, JsCast};
|
||||
@@ -79,8 +81,8 @@ pub struct HydrateSharedContext {
|
||||
id: AtomicUsize,
|
||||
is_hydrating: AtomicBool,
|
||||
during_hydration: AtomicBool,
|
||||
errors: Lazy<Vec<(SerializedDataId, ErrorId, Error)>>,
|
||||
incomplete: Lazy<Vec<SerializedDataId>>,
|
||||
errors: LazyLock<Vec<(SerializedDataId, ErrorId, Error)>>,
|
||||
incomplete: LazyLock<Vec<SerializedDataId>>,
|
||||
}
|
||||
|
||||
impl HydrateSharedContext {
|
||||
@@ -90,8 +92,8 @@ impl HydrateSharedContext {
|
||||
id: AtomicUsize::new(0),
|
||||
is_hydrating: AtomicBool::new(true),
|
||||
during_hydration: AtomicBool::new(true),
|
||||
errors: Lazy::new(serialized_errors),
|
||||
incomplete: Lazy::new(incomplete_chunks),
|
||||
errors: LazyLock::new(serialized_errors),
|
||||
incomplete: LazyLock::new(incomplete_chunks),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,8 +106,8 @@ impl HydrateSharedContext {
|
||||
id: AtomicUsize::new(0),
|
||||
is_hydrating: AtomicBool::new(false),
|
||||
during_hydration: AtomicBool::new(true),
|
||||
errors: Lazy::new(serialized_errors),
|
||||
incomplete: Lazy::new(incomplete_chunks),
|
||||
errors: LazyLock::new(serialized_errors),
|
||||
incomplete: LazyLock::new(incomplete_chunks),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
actix-http = "3.9"
|
||||
actix-files = "0.6"
|
||||
actix-web = "4.9"
|
||||
futures = "0.3.31"
|
||||
actix-http = { workspace = true, default-features = true }
|
||||
actix-files = { workspace = true, default-features = true }
|
||||
actix-web = { workspace = true, default-features = false }
|
||||
futures = { workspace = true, default-features = true }
|
||||
any_spawner = { workspace = true, features = ["tokio"] }
|
||||
hydration_context = { workspace = true }
|
||||
leptos = { workspace = true, features = ["nonce", "ssr"] }
|
||||
@@ -20,20 +20,21 @@ leptos_integration_utils = { workspace = true }
|
||||
leptos_macro = { workspace = true, features = ["actix"] }
|
||||
leptos_meta = { workspace = true, features = ["nonce"] }
|
||||
leptos_router = { workspace = true, features = ["ssr"] }
|
||||
server_fn = { workspace = true, features = ["actix"] }
|
||||
server_fn = { workspace = true, features = ["actix-no-default"] }
|
||||
tachys = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
parking_lot = "0.12.3"
|
||||
tracing = { version = "0.1", optional = true }
|
||||
tokio = { version = "1.43", features = ["rt", "fs"] }
|
||||
send_wrapper = "0.6.0"
|
||||
dashmap = "6"
|
||||
once_cell = "1"
|
||||
serde_json = { workspace = true , default-features = true }
|
||||
parking_lot = { workspace = true, default-features = true }
|
||||
tracing = { optional = true , workspace = true, default-features = true }
|
||||
tokio = { features = ["rt", "fs"] , workspace = true, default-features = true }
|
||||
send_wrapper = { workspace = true, default-features = true }
|
||||
dashmap = { workspace = true, default-features = true }
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--generate-link-to-definition"]
|
||||
|
||||
[features]
|
||||
default = ["actix-default"]
|
||||
actix-default = ["actix-web/default"]
|
||||
islands-router = ["tachys/islands"]
|
||||
tracing = ["dep:tracing"]
|
||||
|
||||
|
||||
@@ -38,7 +38,6 @@ use leptos_router::{
|
||||
static_routes::{RegenerationFn, ResolvedStaticPath},
|
||||
ExpandOptionals, Method, PathSegment, RouteList, RouteListing, SsrMode,
|
||||
};
|
||||
use once_cell::sync::Lazy;
|
||||
use parking_lot::RwLock;
|
||||
use send_wrapper::SendWrapper;
|
||||
use server_fn::{
|
||||
@@ -51,7 +50,7 @@ use std::{
|
||||
future::Future,
|
||||
ops::{Deref, DerefMut},
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
sync::{Arc, LazyLock},
|
||||
};
|
||||
|
||||
/// This struct lets you define headers and override the status of the Response from an Element or a Server Function
|
||||
@@ -283,6 +282,7 @@ pub fn redirect(path: &str) {
|
||||
/// // call ServerFn::register() for each of the server functions you've defined
|
||||
/// }
|
||||
///
|
||||
/// # #[cfg(feature = "default")]
|
||||
/// #[actix_web::main]
|
||||
/// async fn main() -> std::io::Result<()> {
|
||||
/// // make sure you actually register your server functions
|
||||
@@ -298,6 +298,8 @@ pub fn redirect(path: &str) {
|
||||
/// .run()
|
||||
/// .await
|
||||
/// }
|
||||
/// # #[cfg(not(feature = "default"))]
|
||||
/// # fn main() {}
|
||||
/// ```
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
@@ -443,6 +445,7 @@ pub fn handle_server_fns_with_context(
|
||||
/// view! { <main>"Hello, world!"</main> }
|
||||
/// }
|
||||
///
|
||||
/// # #[cfg(feature = "default")]
|
||||
/// #[actix_web::main]
|
||||
/// async fn main() -> std::io::Result<()> {
|
||||
/// let conf = get_configuration(Some("Cargo.toml")).unwrap();
|
||||
@@ -462,6 +465,8 @@ pub fn handle_server_fns_with_context(
|
||||
/// .run()
|
||||
/// .await
|
||||
/// }
|
||||
/// # #[cfg(not(feature = "default"))]
|
||||
/// # fn main() {}
|
||||
/// ```
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
@@ -500,6 +505,7 @@ where
|
||||
/// view! { <main>"Hello, world!"</main> }
|
||||
/// }
|
||||
///
|
||||
/// # #[cfg(feature = "default")]
|
||||
/// #[actix_web::main]
|
||||
/// async fn main() -> std::io::Result<()> {
|
||||
/// let conf = get_configuration(Some("Cargo.toml")).unwrap();
|
||||
@@ -522,6 +528,9 @@ where
|
||||
/// .run()
|
||||
/// .await
|
||||
/// }
|
||||
///
|
||||
/// # #[cfg(not(feature = "default"))]
|
||||
/// # fn main() {}
|
||||
/// ```
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
@@ -558,6 +567,7 @@ where
|
||||
/// view! { <main>"Hello, world!"</main> }
|
||||
/// }
|
||||
///
|
||||
/// # #[cfg(feature = "default")]
|
||||
/// #[actix_web::main]
|
||||
/// async fn main() -> std::io::Result<()> {
|
||||
/// let conf = get_configuration(Some("Cargo.toml")).unwrap();
|
||||
@@ -577,6 +587,8 @@ where
|
||||
/// .run()
|
||||
/// .await
|
||||
/// }
|
||||
/// # #[cfg(not(feature = "default"))]
|
||||
/// # fn main() {}
|
||||
/// ```
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
@@ -1210,8 +1222,8 @@ impl StaticRouteGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
static STATIC_HEADERS: Lazy<DashMap<String, ResponseOptions>> =
|
||||
Lazy::new(DashMap::new);
|
||||
static STATIC_HEADERS: LazyLock<DashMap<String, ResponseOptions>> =
|
||||
LazyLock::new(DashMap::new);
|
||||
|
||||
fn was_404(owner: &Owner) -> bool {
|
||||
let resp = owner.with(|| expect_context::<ResponseOptions>());
|
||||
|
||||
@@ -4,18 +4,18 @@ authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Axum integrations for the Leptos web framework."
|
||||
version = "0.8.0-rc3"
|
||||
version = { workspace = true }
|
||||
rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
any_spawner = { workspace = true, features = ["tokio"] }
|
||||
hydration_context = { workspace = true }
|
||||
axum = { version = "0.8.1", default-features = false, features = [
|
||||
axum = { default-features = false, features = [
|
||||
"matched-path",
|
||||
] }
|
||||
dashmap = "6"
|
||||
futures = "0.3.31"
|
||||
], workspace = true }
|
||||
dashmap = { workspace = true, default-features = true }
|
||||
futures = { workspace = true, default-features = true }
|
||||
leptos = { workspace = true, features = ["nonce", "ssr"] }
|
||||
server_fn = { workspace = true, features = ["axum-no-default"] }
|
||||
leptos_macro = { workspace = true, features = ["axum"] }
|
||||
@@ -23,16 +23,18 @@ leptos_meta = { workspace = true, features = ["ssr", "nonce"] }
|
||||
leptos_router = { workspace = true, features = ["ssr"] }
|
||||
leptos_integration_utils = { workspace = true }
|
||||
tachys = { workspace = true }
|
||||
once_cell = "1"
|
||||
parking_lot = "0.12.3"
|
||||
tokio = { version = "1.43", default-features = false }
|
||||
tower = { version = "0.5.1", features = ["util"] }
|
||||
tower-http = "0.6.2"
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
parking_lot = { workspace = true, default-features = true }
|
||||
tokio = { default-features = false, workspace = true }
|
||||
tower = { features = ["util"], workspace = true, default-features = true }
|
||||
tower-http = { workspace = true, default-features = true }
|
||||
tracing = { optional = true, workspace = true, default-features = true }
|
||||
|
||||
[dev-dependencies]
|
||||
axum = "0.8.1"
|
||||
tokio = { version = "1.43", features = ["net", "rt-multi-thread"] }
|
||||
axum = { workspace = true, default-features = true }
|
||||
tokio = { features = [
|
||||
"net",
|
||||
"rt-multi-thread",
|
||||
], workspace = true, default-features = true }
|
||||
|
||||
[features]
|
||||
wasm = []
|
||||
|
||||
@@ -69,12 +69,12 @@ use leptos_router::{
|
||||
static_routes::RegenerationFn, ExpandOptionals, PathSegment, RouteList,
|
||||
RouteListing, SsrMode,
|
||||
};
|
||||
#[cfg(feature = "default")]
|
||||
use once_cell::sync::Lazy;
|
||||
use parking_lot::RwLock;
|
||||
use server_fn::{error::ServerFnErrorErr, redirect::REDIRECT_HEADER};
|
||||
#[cfg(feature = "default")]
|
||||
use std::path::Path;
|
||||
#[cfg(feature = "default")]
|
||||
use std::sync::LazyLock;
|
||||
use std::{collections::HashSet, fmt::Debug, io, pin::Pin, sync::Arc};
|
||||
#[cfg(feature = "default")]
|
||||
use tower::util::ServiceExt;
|
||||
@@ -590,7 +590,7 @@ where
|
||||
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
|
||||
/// to route it using [leptos_router], serving an HTML stream of your application.
|
||||
///
|
||||
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network
|
||||
/// This version allows us to pass Axum State/Extension/Extractor or other info from Axum or network
|
||||
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
|
||||
/// the data to leptos in a closure. An example is below
|
||||
/// ```
|
||||
@@ -796,7 +796,7 @@ where
|
||||
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve before
|
||||
/// sending down its HTML. The app will become interactive once it has fully loaded.
|
||||
///
|
||||
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network
|
||||
/// This version allows us to pass Axum State/Extension/Extractor or other info from Axum or network
|
||||
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
|
||||
/// the data to leptos in a closure. An example is below
|
||||
/// ```
|
||||
@@ -879,7 +879,8 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_response_inner<IV>(
|
||||
/// Can be used in conjunction with a custom [file_and_error_handler_with_context] to process an Axum [Request](axum::extract::Request) into an Axum [Response](axum::response::Response)
|
||||
pub fn handle_response_inner<IV>(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl FnOnce() -> IV + Send + 'static,
|
||||
req: Request<Body>,
|
||||
@@ -1022,7 +1023,7 @@ where
|
||||
/// to route it using [leptos_router], asynchronously rendering an HTML page after all
|
||||
/// `async` resources have loaded.
|
||||
///
|
||||
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network
|
||||
/// This version allows us to pass Axum State/Extension/Extractor or other info from Axum or network
|
||||
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
|
||||
/// the data to leptos in a closure. An example is below
|
||||
/// ```
|
||||
@@ -1089,7 +1090,7 @@ where
|
||||
/// to route it using [leptos_router], asynchronously rendering an HTML page after all
|
||||
/// `async` resources have loaded.
|
||||
///
|
||||
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network
|
||||
/// This version allows us to pass Axum State/Extension/Extractor or other info from Axum or network
|
||||
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
|
||||
/// the data to leptos in a closure. An example is below
|
||||
/// ```
|
||||
@@ -1521,8 +1522,8 @@ impl StaticRouteGenerator {
|
||||
}
|
||||
|
||||
#[cfg(feature = "default")]
|
||||
static STATIC_HEADERS: Lazy<DashMap<String, ResponseOptions>> =
|
||||
Lazy::new(DashMap::new);
|
||||
static STATIC_HEADERS: LazyLock<DashMap<String, ResponseOptions>> =
|
||||
LazyLock::new(DashMap::new);
|
||||
|
||||
#[cfg(feature = "default")]
|
||||
fn was_404(owner: &Owner) -> bool {
|
||||
|
||||
@@ -9,7 +9,7 @@ rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
futures = "0.3.31"
|
||||
futures = { workspace = true, default-features = true }
|
||||
hydration_context = { workspace = true }
|
||||
leptos = { workspace = true, features = ["nonce"] }
|
||||
leptos_meta = { workspace = true, features = ["ssr"] }
|
||||
|
||||
@@ -15,8 +15,8 @@ any_spawner = { workspace = true, features = [
|
||||
"wasm-bindgen",
|
||||
"futures-executor",
|
||||
] }
|
||||
base64 = { version = "0.22.1", optional = true }
|
||||
cfg-if = "1.0"
|
||||
base64 = { optional = true, workspace = true, default-features = true }
|
||||
cfg-if = { workspace = true, default-features = true }
|
||||
hydration_context = { workspace = true }
|
||||
either_of = { workspace = true }
|
||||
leptos_dom = { workspace = true }
|
||||
@@ -24,38 +24,38 @@ leptos_hot_reload = { workspace = true }
|
||||
leptos_macro = { workspace = true }
|
||||
leptos_server = { workspace = true, features = ["tachys"] }
|
||||
leptos_config = { workspace = true }
|
||||
leptos-spin-macro = { version = "0.2.0", optional = true }
|
||||
leptos-spin-macro = { optional = true , workspace = true, default-features = true }
|
||||
oco_ref = { workspace = true }
|
||||
or_poisoned = { workspace = true }
|
||||
paste = "1.0"
|
||||
rand = { version = "0.8.5", optional = true }
|
||||
# NOTE: While not used directly, `getrandom`'s `js` feature is needed when `rand` is used on WASM to
|
||||
paste = { workspace = true, default-features = true }
|
||||
rand = { optional = true , workspace = true, default-features = true }
|
||||
# NOTE: While not used directly, `getrandom`'s `wasm_js` feature is needed when `rand` is used on WASM to
|
||||
# avoid a compilation error
|
||||
getrandom = { version = "0.2", optional = true }
|
||||
getrandom = { optional = true , workspace = true, default-features = true }
|
||||
reactive_graph = { workspace = true, features = ["serde"] }
|
||||
rustc-hash = "2.0"
|
||||
rustc-hash = { workspace = true, default-features = true }
|
||||
tachys = { workspace = true, features = [
|
||||
"reactive_graph",
|
||||
"reactive_stores",
|
||||
"oco",
|
||||
] }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
typed-builder = { workspace = true }
|
||||
typed-builder-macro = "0.21.0"
|
||||
serde = "1.0"
|
||||
serde_json = { version = "1.0", optional = true }
|
||||
thiserror = { workspace = true, default-features = true }
|
||||
tracing = { optional = true, workspace = true, default-features = true }
|
||||
typed-builder = { workspace = true, default-features = true }
|
||||
typed-builder-macro = { workspace = true, default-features = true }
|
||||
serde = { workspace = true, default-features = true }
|
||||
serde_json = { optional = true, workspace = true, default-features = true }
|
||||
server_fn = { workspace = true, features = ["form-redirects", "browser"] }
|
||||
web-sys = { version = "0.3.72", features = [
|
||||
web-sys = { features = [
|
||||
"ShadowRoot",
|
||||
"ShadowRootInit",
|
||||
"ShadowRootMode",
|
||||
] }
|
||||
wasm-bindgen = { workspace = true }
|
||||
serde_qs = "0.14.0"
|
||||
slotmap = "1.0"
|
||||
futures = "0.3.31"
|
||||
send_wrapper = "0.6.0"
|
||||
], workspace = true, default-features = true }
|
||||
wasm-bindgen = { workspace = true, default-features = true }
|
||||
serde_qs = { workspace = true, default-features = true }
|
||||
slotmap = { workspace = true, default-features = true }
|
||||
futures = { workspace = true, default-features = true }
|
||||
send_wrapper = { workspace = true, default-features = true }
|
||||
|
||||
[features]
|
||||
hydration = [
|
||||
@@ -64,13 +64,13 @@ hydration = [
|
||||
"hydration_context/browser",
|
||||
"leptos_dom/hydration",
|
||||
]
|
||||
csr = ["leptos_macro/csr", "reactive_graph/effects", "getrandom?/js"]
|
||||
csr = ["leptos_macro/csr", "reactive_graph/effects", "getrandom?/wasm_js"]
|
||||
hydrate = [
|
||||
"leptos_macro/hydrate",
|
||||
"hydration",
|
||||
"tachys/hydrate",
|
||||
"reactive_graph/effects",
|
||||
"getrandom?/js",
|
||||
"getrandom?/wasm_js",
|
||||
]
|
||||
default-tls = ["server_fn/default-tls"]
|
||||
rustls = ["server_fn/rustls"]
|
||||
@@ -99,9 +99,15 @@ trace-component-props = [
|
||||
"leptos_dom/trace-component-props",
|
||||
]
|
||||
delegation = ["tachys/delegation"]
|
||||
islands-router = ["tachys/mark_branches"]
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { features = ["rt-multi-thread", "macros"] , workspace = true, default-features = true }
|
||||
tokio-test = { workspace = true, default-features = true }
|
||||
any_spawner = { workspace = true, features = ["futures-executor", "tokio"] }
|
||||
|
||||
[build-dependencies]
|
||||
rustc_version = "0.4.1"
|
||||
rustc_version = { workspace = true, default-features = true }
|
||||
|
||||
# Having an erasure feature rather than normal --cfg erase_components for the proc macro crate is a workaround for this rust issue:
|
||||
# https://github.com/rust-lang/cargo/issues/4423
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
use rustc_version::{version_meta, Channel};
|
||||
|
||||
fn main() {
|
||||
let target = std::env::var("TARGET").unwrap_or_default();
|
||||
|
||||
// Set cfg flags depending on release channel
|
||||
if matches!(version_meta().unwrap().channel, Channel::Nightly) {
|
||||
println!("cargo:rustc-cfg=rustc_nightly");
|
||||
}
|
||||
// Set cfg flag for getrandom wasm_js
|
||||
if target == "wasm32-unknown-unknown" {
|
||||
// Set a custom cfg flag for wasm builds
|
||||
println!("cargo:rustc-cfg=getrandom_backend=\"wasm_js\"");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ type BoxedChildrenFn = Box<dyn Fn() -> AnyView + Send>;
|
||||
///
|
||||
/// Different component types take different types for their `children` prop, some of which cannot
|
||||
/// be directly constructed. Using `ToChildren` allows the component user to pass children without
|
||||
/// explicity constructing the correct type.
|
||||
/// explicitly constructing the correct type.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
|
||||
@@ -255,7 +255,7 @@ where
|
||||
) -> Result<Self, serde_qs::Error>;
|
||||
}
|
||||
|
||||
/// Errors that can arise when coverting from an HTML event or form into a Rust data type.
|
||||
/// Errors that can arise when converting from an HTML event or form into a Rust data type.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum FromFormDataError {
|
||||
/// Could not find a `<form>` connected to the event.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
((root, pkg_path, output_name, wasm_output_name) => {
|
||||
let MOST_RECENT_CHILDREN_CB;
|
||||
let MOST_RECENT_CHILDREN_CB = [];
|
||||
|
||||
function idle(c) {
|
||||
if ("requestIdleCallback" in window) {
|
||||
@@ -22,12 +22,18 @@
|
||||
traverse(child, children);
|
||||
}
|
||||
} else {
|
||||
if(tag === 'leptos-children') {
|
||||
MOST_RECENT_CHILDREN_CB = node.$$on_hydrate;
|
||||
if (tag === 'leptos-children') {
|
||||
MOST_RECENT_CHILDREN_CB.push(node.$$on_hydrate);
|
||||
for(const child of node.children) {
|
||||
traverse(child);
|
||||
};
|
||||
// un-set the "most recent children"
|
||||
MOST_RECENT_CHILDREN_CB.pop();
|
||||
} else {
|
||||
for(const child of node.children) {
|
||||
traverse(child);
|
||||
};
|
||||
}
|
||||
for(const child of node.children) {
|
||||
traverse(child);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,8 +43,9 @@
|
||||
function hydrateIsland(el, id, mod) {
|
||||
const islandFn = mod[id];
|
||||
if (islandFn) {
|
||||
if (MOST_RECENT_CHILDREN_CB) {
|
||||
MOST_RECENT_CHILDREN_CB();
|
||||
const children_cb = MOST_RECENT_CHILDREN_CB[MOST_RECENT_CHILDREN_CB.length-1];
|
||||
if (children_cb) {
|
||||
children_cb();
|
||||
}
|
||||
islandFn(el);
|
||||
} else {
|
||||
@@ -48,7 +55,7 @@
|
||||
idle(() => {
|
||||
import(`${root}/${pkg_path}/${output_name}.js`)
|
||||
.then(mod => {
|
||||
mod.default(`${root}/${pkg_path}/${wasm_output_name}.wasm`).then(() => {
|
||||
mod.default({module_or_path: `${root}/${pkg_path}/${wasm_output_name}.wasm`}).then(() => {
|
||||
mod.hydrate();
|
||||
hydrateIslands(document.body, mod);
|
||||
});
|
||||
|
||||
@@ -17,6 +17,10 @@ window.addEventListener("popstate", async (ev) => {
|
||||
});
|
||||
|
||||
window.addEventListener("submit", async (ev) => {
|
||||
if (ev.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
|
||||
const req = submitToReq(ev);
|
||||
if(!req) {
|
||||
return;
|
||||
@@ -194,6 +198,15 @@ function diffRange(oldDocument, oldRoot, newDocument, newRoot, oldEnd, newEnd) {
|
||||
else if (oldNode.nodeType === Node.TEXT_NODE) {
|
||||
oldNode.textContent = newNode.textContent;
|
||||
}
|
||||
// islands should not be diffed on the client, because we do not want to overwrite client-side state
|
||||
// but their children should be diffed still, because they could contain new server content
|
||||
else if (oldNode.nodeType === Node.ELEMENT_NODE && oldNode.tagName === "LEPTOS-ISLAND") {
|
||||
// TODO: diff the leptos-children
|
||||
|
||||
// skip over leptos-island otherwise
|
||||
oldDocWalker.nextSibling();
|
||||
newDocWalker.nextSibling();
|
||||
}
|
||||
// if it's an element, replace if it's a different tag, or update attributes
|
||||
else if (oldNode.nodeType === Node.ELEMENT_NODE) {
|
||||
diffElement(oldNode, newNode);
|
||||
|
||||
@@ -83,6 +83,10 @@ pub fn HydrationScripts(
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
leptos::logging::error!(
|
||||
"File hashing is active but no hash file was found"
|
||||
);
|
||||
}
|
||||
} else if std::option_env!("LEPTOS_OUTPUT_NAME").is_none() {
|
||||
wasm_file_name.push_str("_bg");
|
||||
@@ -107,21 +111,19 @@ pub fn HydrationScripts(
|
||||
.unwrap_or_default();
|
||||
|
||||
let root = root.unwrap_or_default();
|
||||
use_context::<IslandsRouterNavigation>().is_none().then(|| {
|
||||
view! {
|
||||
<link rel="modulepreload" href=format!("{root}/{pkg_path}/{js_file_name}.js") nonce=nonce.clone()/>
|
||||
<link
|
||||
rel="preload"
|
||||
href=format!("{root}/{pkg_path}/{wasm_file_name}.wasm")
|
||||
r#as="fetch"
|
||||
r#type="application/wasm"
|
||||
crossorigin=nonce.clone().unwrap_or_default()
|
||||
/>
|
||||
<script type="module" nonce=nonce>
|
||||
{format!("{script}({root:?}, {pkg_path:?}, {js_file_name:?}, {wasm_file_name:?});{islands_router}")}
|
||||
</script>
|
||||
}
|
||||
})
|
||||
view! {
|
||||
<link rel="modulepreload" href=format!("{root}/{pkg_path}/{js_file_name}.js") nonce=nonce.clone()/>
|
||||
<link
|
||||
rel="preload"
|
||||
href=format!("{root}/{pkg_path}/{wasm_file_name}.wasm")
|
||||
r#as="fetch"
|
||||
r#type="application/wasm"
|
||||
crossorigin=nonce.clone().unwrap_or_default()
|
||||
/>
|
||||
<script type="module" nonce=nonce>
|
||||
{format!("{script}({root:?}, {pkg_path:?}, {js_file_name:?}, {wasm_file_name:?});{islands_router}")}
|
||||
</script>
|
||||
}
|
||||
}
|
||||
|
||||
/// If this is provided via context, it means that you are using the islands router and
|
||||
|
||||
@@ -7,7 +7,7 @@ ws.onmessage = (ev) => {
|
||||
let found = false;
|
||||
document.querySelectorAll("link").forEach((link) => {
|
||||
if (link.getAttribute('href').includes(msg.css)) {
|
||||
let newHref = '/' + msg.css + '?version=' + new Date().getMilliseconds();
|
||||
let newHref = '/' + msg.css + '?version=' + Date.now();
|
||||
link.setAttribute('href', newHref);
|
||||
found = true;
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ impl<T: RenderHtml> RenderHtml for View<T> {
|
||||
type Owned = View<T::Owned>;
|
||||
|
||||
const MIN_LENGTH: usize = <T as RenderHtml>::MIN_LENGTH;
|
||||
const EXISTS: bool = <T as RenderHtml>::EXISTS;
|
||||
|
||||
async fn resolve(self) -> Self::AsyncOutput {
|
||||
self.inner.resolve().await
|
||||
@@ -107,9 +108,14 @@ impl<T: RenderHtml> RenderHtml for View<T> {
|
||||
mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) {
|
||||
#[cfg(all(debug_assertions, feature = "nightly", rustc_nightly))]
|
||||
let vm = self.view_marker.to_owned();
|
||||
#[cfg(all(debug_assertions, feature = "nightly", rustc_nightly))]
|
||||
#[cfg(debug_assertions)]
|
||||
let vm = if option_env!("LEPTOS_WATCH").is_some() {
|
||||
self.view_marker.to_owned()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
if let Some(vm) = vm.as_ref() {
|
||||
buf.push_str(&format!("<!--hot-reload|{vm}|open-->"));
|
||||
}
|
||||
@@ -122,7 +128,7 @@ impl<T: RenderHtml> RenderHtml for View<T> {
|
||||
extra_attrs,
|
||||
);
|
||||
|
||||
#[cfg(all(debug_assertions, feature = "nightly", rustc_nightly))]
|
||||
#[cfg(debug_assertions)]
|
||||
if let Some(vm) = vm.as_ref() {
|
||||
buf.push_str(&format!("<!--hot-reload|{vm}|close-->"));
|
||||
}
|
||||
@@ -138,9 +144,14 @@ impl<T: RenderHtml> RenderHtml for View<T> {
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
#[cfg(all(debug_assertions, feature = "nightly", rustc_nightly))]
|
||||
let vm = self.view_marker.to_owned();
|
||||
#[cfg(all(debug_assertions, feature = "nightly", rustc_nightly))]
|
||||
#[cfg(debug_assertions)]
|
||||
let vm = if option_env!("LEPTOS_WATCH").is_some() {
|
||||
self.view_marker.to_owned()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
if let Some(vm) = vm.as_ref() {
|
||||
buf.push_sync(&format!("<!--hot-reload|{vm}|open-->"));
|
||||
}
|
||||
@@ -153,7 +164,7 @@ impl<T: RenderHtml> RenderHtml for View<T> {
|
||||
extra_attrs,
|
||||
);
|
||||
|
||||
#[cfg(all(debug_assertions, feature = "nightly", rustc_nightly))]
|
||||
#[cfg(debug_assertions)]
|
||||
if let Some(vm) = vm.as_ref() {
|
||||
buf.push_sync(&format!("<!--hot-reload|{vm}|close-->"));
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use base64::{
|
||||
engine::{self, general_purpose},
|
||||
Engine,
|
||||
};
|
||||
use rand::{thread_rng, RngCore};
|
||||
use rand::{rng, RngCore};
|
||||
use std::{fmt::Display, ops::Deref, sync::Arc};
|
||||
use tachys::html::attribute::AttributeValue;
|
||||
|
||||
@@ -171,9 +171,9 @@ const NONCE_ENGINE: engine::GeneralPurpose =
|
||||
impl Nonce {
|
||||
/// Generates a new nonce from 16 bytes (128 bits) of random data.
|
||||
pub fn new() -> Self {
|
||||
let mut thread_rng = thread_rng();
|
||||
let mut rng = rng();
|
||||
let mut bytes = [0; 16];
|
||||
thread_rng.fill_bytes(&mut bytes);
|
||||
rng.fill_bytes(&mut bytes);
|
||||
Nonce(NONCE_ENGINE.encode(bytes).into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use std::sync::Arc;
|
||||
///
|
||||
/// Useful for inserting modals and tooltips outside of a cropping layout.
|
||||
/// If no mount point is given, the portal is inserted in `document.body`;
|
||||
/// it is wrapped in a `<div>` unless `is_svg` is `true` in which case it's wrappend in a `<g>`.
|
||||
/// it is wrapped in a `<div>` unless `is_svg` is `true` in which case it's wrapped in a `<g>`.
|
||||
/// Setting `use_shadow` to `true` places the element in a shadow root to isolate styles.
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
|
||||
#[component]
|
||||
@@ -42,11 +42,15 @@ where
|
||||
let children = children.into_inner();
|
||||
|
||||
Effect::new(move |_| {
|
||||
let tag = if is_svg { "g" } else { "div" };
|
||||
|
||||
let container = document()
|
||||
.create_element(tag)
|
||||
.expect("element creation to work");
|
||||
let container = if is_svg {
|
||||
document()
|
||||
.create_element_ns(Some("http://www.w3.org/2000/svg"), "g")
|
||||
.expect("SVG element creation to work")
|
||||
} else {
|
||||
document()
|
||||
.create_element("div")
|
||||
.expect("HTML element creation to work")
|
||||
};
|
||||
|
||||
let render_root = if use_shadow {
|
||||
container
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||
use tachys::prelude::IntoAttributeValue;
|
||||
|
||||
/// Describes a value that is either a static or a reactive string, i.e.,
|
||||
/// a [`String`], a [`&str`], or a reactive `Fn() -> String`.
|
||||
/// a [`String`], a [`&str`], a `Signal` or a reactive `Fn() -> String`.
|
||||
#[derive(Clone)]
|
||||
pub struct TextProp(Arc<dyn Fn() -> Oco<'static, str> + Send + Sync>);
|
||||
|
||||
@@ -82,3 +82,93 @@ impl IntoAttributeValue for TextProp {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! textprop_reactive {
|
||||
($name:ident, <$($gen:ident),*>, $v:ty, $( $where_clause:tt )*) =>
|
||||
{
|
||||
#[allow(deprecated)]
|
||||
impl<$($gen),*> From<$name<$($gen),*>> for TextProp
|
||||
where
|
||||
$v: Into<Oco<'static, str>> + Clone + Send + Sync + 'static,
|
||||
$($where_clause)*
|
||||
{
|
||||
#[inline(always)]
|
||||
fn from(s: $name<$($gen),*>) -> Self {
|
||||
TextProp(Arc::new(move || s.get().into()))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
mod stable {
|
||||
use super::TextProp;
|
||||
use oco_ref::Oco;
|
||||
#[allow(deprecated)]
|
||||
use reactive_graph::wrappers::read::MaybeSignal;
|
||||
use reactive_graph::{
|
||||
computed::{ArcMemo, Memo},
|
||||
owner::Storage,
|
||||
signal::{ArcReadSignal, ArcRwSignal, ReadSignal, RwSignal},
|
||||
traits::Get,
|
||||
wrappers::read::{ArcSignal, Signal},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
textprop_reactive!(
|
||||
RwSignal,
|
||||
<V, S>,
|
||||
V,
|
||||
RwSignal<V, S>: Get<Value = V>,
|
||||
S: Storage<V> + Storage<Option<V>>,
|
||||
S: Send + Sync + 'static,
|
||||
);
|
||||
textprop_reactive!(
|
||||
ReadSignal,
|
||||
<V, S>,
|
||||
V,
|
||||
ReadSignal<V, S>: Get<Value = V>,
|
||||
S: Storage<V> + Storage<Option<V>>,
|
||||
S: Send + Sync + 'static,
|
||||
);
|
||||
textprop_reactive!(
|
||||
Memo,
|
||||
<V, S>,
|
||||
V,
|
||||
Memo<V, S>: Get<Value = V>,
|
||||
S: Storage<V> + Storage<Option<V>>,
|
||||
S: Send + Sync + 'static,
|
||||
);
|
||||
textprop_reactive!(
|
||||
Signal,
|
||||
<V, S>,
|
||||
V,
|
||||
Signal<V, S>: Get<Value = V>,
|
||||
S: Storage<V> + Storage<Option<V>>,
|
||||
S: Send + Sync + 'static,
|
||||
);
|
||||
textprop_reactive!(
|
||||
MaybeSignal,
|
||||
<V, S>,
|
||||
V,
|
||||
MaybeSignal<V, S>: Get<Value = V>,
|
||||
S: Storage<V> + Storage<Option<V>>,
|
||||
S: Send + Sync + 'static,
|
||||
);
|
||||
textprop_reactive!(ArcRwSignal, <V>, V, ArcRwSignal<V>: Get<Value = V>);
|
||||
textprop_reactive!(ArcReadSignal, <V>, V, ArcReadSignal<V>: Get<Value = V>);
|
||||
textprop_reactive!(ArcMemo, <V>, V, ArcMemo<V>: Get<Value = V>);
|
||||
textprop_reactive!(ArcSignal, <V>, V, ArcSignal<V>: Get<Value = V>);
|
||||
}
|
||||
|
||||
/// Extension trait for `Option<TextProp>`
|
||||
pub trait OptionTextPropExt {
|
||||
/// Accesses the current value of the `Option<TextProp>` as an `Option<Oco<'static, str>>`.
|
||||
fn get(&self) -> Option<Oco<'static, str>>;
|
||||
}
|
||||
|
||||
impl OptionTextPropExt for Option<TextProp> {
|
||||
fn get(&self) -> Option<Oco<'static, str>> {
|
||||
self.as_ref().map(|text_prop| text_prop.get())
|
||||
}
|
||||
}
|
||||
|
||||
79
leptos/tests/pr_4061.rs
Normal file
79
leptos/tests/pr_4061.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
mod imports {
|
||||
pub use any_spawner::Executor;
|
||||
pub use futures::StreamExt;
|
||||
pub use leptos::prelude::*;
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::test]
|
||||
async fn chain_await_resource() {
|
||||
use imports::*;
|
||||
|
||||
_ = Executor::init_tokio();
|
||||
let owner = Owner::new();
|
||||
owner.set();
|
||||
|
||||
let (rs, ws) = signal(0);
|
||||
let source = Resource::new(
|
||||
|| (),
|
||||
move |_| async move {
|
||||
#[cfg(feature = "ssr")]
|
||||
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
|
||||
1
|
||||
},
|
||||
);
|
||||
let consuming = Resource::new(
|
||||
|| (),
|
||||
move |_| async move {
|
||||
let result = source.await;
|
||||
ws.update(|s| *s += 1);
|
||||
result
|
||||
},
|
||||
);
|
||||
let app = view! {
|
||||
<Suspense>{
|
||||
move || {
|
||||
Suspend::new(async move {
|
||||
consuming.await;
|
||||
rs.get()
|
||||
})
|
||||
}
|
||||
}</Suspense>
|
||||
};
|
||||
|
||||
assert_eq!(app.to_html_stream_in_order().collect::<String>().await, "1");
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::test]
|
||||
async fn chain_no_await_resource() {
|
||||
use imports::*;
|
||||
|
||||
_ = Executor::init_tokio();
|
||||
let owner = Owner::new();
|
||||
owner.set();
|
||||
|
||||
let (rs, ws) = signal(0);
|
||||
let source = Resource::new(|| (), move |_| async move { 1 });
|
||||
let consuming = Resource::new(
|
||||
|| (),
|
||||
move |_| async move {
|
||||
let result = source.await;
|
||||
ws.update(|s| *s += 1);
|
||||
result
|
||||
},
|
||||
);
|
||||
let app = view! {
|
||||
<Suspense>{
|
||||
move || {
|
||||
Suspend::new(async move {
|
||||
consuming.await;
|
||||
rs.get()
|
||||
})
|
||||
}
|
||||
}</Suspense>
|
||||
};
|
||||
|
||||
assert_eq!(app.to_html_stream_in_order().collect::<String>().await, "1");
|
||||
}
|
||||
@@ -10,19 +10,19 @@ rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
config = { version = "0.15.8", default-features = false, features = [
|
||||
config = { default-features = false, features = [
|
||||
"toml",
|
||||
"convert-case",
|
||||
] }
|
||||
regex = "1.11"
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
thiserror = { workspace = true }
|
||||
typed-builder = { workspace = true }
|
||||
] , workspace = true }
|
||||
regex = { workspace = true, default-features = true }
|
||||
serde = { features = ["derive", "rc"] , workspace = true, default-features = true }
|
||||
thiserror = { workspace = true , default-features = true }
|
||||
typed-builder = { workspace = true , default-features = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.43", features = ["rt", "macros"] }
|
||||
tempfile = "3.14"
|
||||
temp-env = { version = "0.3.6", features = ["async_closure"] }
|
||||
tokio = { features = ["rt", "macros"] , workspace = true, default-features = true }
|
||||
tempfile = { workspace = true, default-features = true }
|
||||
temp-env = { features = ["async_closure"] , workspace = true, default-features = true }
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--generate-link-to-definition"]
|
||||
|
||||
@@ -12,19 +12,20 @@ edition.workspace = true
|
||||
tachys = { workspace = true }
|
||||
reactive_graph = { workspace = true }
|
||||
or_poisoned = { workspace = true }
|
||||
js-sys = "0.3.74"
|
||||
send_wrapper = "0.6.0"
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
wasm-bindgen = { workspace = true }
|
||||
serde_json = { version = "1.0", optional = true }
|
||||
serde = { version = "1.0", optional = true }
|
||||
js-sys = { workspace = true, default-features = true }
|
||||
send_wrapper = { workspace = true, default-features = true }
|
||||
tracing = { optional = true , workspace = true, default-features = true }
|
||||
wasm-bindgen = { workspace = true , default-features = true }
|
||||
serde_json = { optional = true , workspace = true, default-features = true }
|
||||
serde = { optional = true , workspace = true, default-features = true }
|
||||
|
||||
[dev-dependencies]
|
||||
leptos = { path = "../leptos" }
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3.72"
|
||||
features = ["Location"]
|
||||
workspace = true
|
||||
default-features = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
@@ -23,6 +23,19 @@ macro_rules! error {
|
||||
($($t:tt)*) => ($crate::logging::console_error(&format_args!($($t)*).to_string()))
|
||||
}
|
||||
|
||||
/// Uses `println!()`-style formatting to log something to the console (in the browser)
|
||||
/// or via `println!()` (if not in the browser), but only if it's a debug build.
|
||||
#[macro_export]
|
||||
macro_rules! debug_log {
|
||||
($($x:tt)*) => {
|
||||
{
|
||||
if cfg!(debug_assertions) {
|
||||
$crate::log!($($x)*)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Uses `println!()`-style formatting to log warnings to the console (in the browser)
|
||||
/// or via `eprintln!()` (if not in the browser), but only if it's a debug build.
|
||||
#[macro_export]
|
||||
@@ -36,6 +49,19 @@ macro_rules! debug_warn {
|
||||
}
|
||||
}
|
||||
|
||||
/// Uses `println!()`-style formatting to log errors to the console (in the browser)
|
||||
/// or via `eprintln!()` (if not in the browser), but only if it's a debug build.
|
||||
#[macro_export]
|
||||
macro_rules! debug_error {
|
||||
($($x:tt)*) => {
|
||||
{
|
||||
if cfg!(debug_assertions) {
|
||||
$crate::error!($($x)*)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fn log_to_stdout() -> bool {
|
||||
cfg!(not(all(
|
||||
target_arch = "wasm32",
|
||||
@@ -55,7 +81,7 @@ pub fn console_log(s: &str) {
|
||||
}
|
||||
|
||||
/// Log a warning to the console (in the browser)
|
||||
/// or via `println!()` (if not in the browser).
|
||||
/// or via `eprintln!()` (if not in the browser).
|
||||
pub fn console_warn(s: &str) {
|
||||
if log_to_stdout() {
|
||||
eprintln!("{s}");
|
||||
@@ -65,7 +91,7 @@ pub fn console_warn(s: &str) {
|
||||
}
|
||||
|
||||
/// Log an error to the console (in the browser)
|
||||
/// or via `println!()` (if not in the browser).
|
||||
/// or via `eprintln!()` (if not in the browser).
|
||||
#[inline(always)]
|
||||
pub fn console_error(s: &str) {
|
||||
if log_to_stdout() {
|
||||
@@ -75,21 +101,29 @@ pub fn console_error(s: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Log an error to the console (in the browser)
|
||||
/// Log a string to the console (in the browser)
|
||||
/// or via `println!()` (if not in the browser), but only in a debug build.
|
||||
#[inline(always)]
|
||||
pub fn console_debug_warn(s: &str) {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
if log_to_stdout() {
|
||||
eprintln!("{s}");
|
||||
} else {
|
||||
web_sys::console::warn_1(&JsValue::from_str(s));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
let _ = s;
|
||||
pub fn console_debug_log(s: &str) {
|
||||
if cfg!(debug_assertions) {
|
||||
console_log(s)
|
||||
}
|
||||
}
|
||||
|
||||
/// Log a warning to the console (in the browser)
|
||||
/// or via `eprintln!()` (if not in the browser), but only in a debug build.
|
||||
#[inline(always)]
|
||||
pub fn console_debug_warn(s: &str) {
|
||||
if cfg!(debug_assertions) {
|
||||
console_warn(s)
|
||||
}
|
||||
}
|
||||
|
||||
/// Log an error to the console (in the browser)
|
||||
/// or via `eprintln!()` (if not in the browser), but only in a debug build.
|
||||
#[inline(always)]
|
||||
pub fn console_debug_error(s: &str) {
|
||||
if cfg!(debug_assertions) {
|
||||
console_error(s)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,19 +10,19 @@ rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
syn = { version = "2.0", features = [
|
||||
anyhow = { workspace = true, default-features = true }
|
||||
serde = { features = ["derive"] , workspace = true, default-features = true }
|
||||
syn = { features = [
|
||||
"full",
|
||||
"parsing",
|
||||
"extra-traits",
|
||||
"visit",
|
||||
"printing",
|
||||
] }
|
||||
quote = "1.0"
|
||||
rstml = "0.12.0"
|
||||
proc-macro2 = { version = "1.0", features = ["span-locations", "nightly"] }
|
||||
parking_lot = "0.12.3"
|
||||
walkdir = "2.5"
|
||||
camino = "1.1"
|
||||
indexmap = "2.6"
|
||||
] , workspace = true, default-features = true }
|
||||
quote = { workspace = true, default-features = true }
|
||||
rstml = { workspace = true, default-features = true }
|
||||
proc-macro2 = { features = ["span-locations", "nightly"] , workspace = true, default-features = true }
|
||||
parking_lot = { workspace = true, default-features = true }
|
||||
walkdir = { workspace = true, default-features = true }
|
||||
camino = { workspace = true, default-features = true }
|
||||
indexmap = { workspace = true, default-features = true }
|
||||
|
||||
@@ -251,93 +251,67 @@ impl LNode {
|
||||
action: PatchAction::ClearChildren,
|
||||
}]
|
||||
} else {
|
||||
let mut a = 0;
|
||||
let mut b = std::cmp::max(old.len(), new.len()) - 1; // min is 0, have checked both have items
|
||||
let width = old.len() + 1;
|
||||
let height = new.len() + 1;
|
||||
let mut mat = vec![0; width * height];
|
||||
#[allow(clippy::needless_range_loop)]
|
||||
for i in 1..width {
|
||||
mat[i] = i;
|
||||
}
|
||||
for i in 1..height {
|
||||
mat[i * width] = i;
|
||||
}
|
||||
for j in 1..height {
|
||||
for i in 1..width {
|
||||
if old[i - 1] == new[j - 1] {
|
||||
mat[j * width + i] = mat[(j - 1) * width + (i - 1)];
|
||||
} else {
|
||||
mat[j * width + i] = (mat[(j - 1) * width + i] + 1)
|
||||
.min(mat[j * width + (i - 1)] + 1)
|
||||
.min(mat[(j - 1) * width + (i - 1)] + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
let (mut i, mut j) = (old.len(), new.len());
|
||||
let mut patches = vec![];
|
||||
// common prefix
|
||||
while a < b {
|
||||
let old = old.get(a);
|
||||
let new = new.get(a);
|
||||
|
||||
match (old, new) {
|
||||
(None, Some(new)) => patches.push(Patch {
|
||||
path: path.to_owned(),
|
||||
action: PatchAction::InsertChild {
|
||||
before: a,
|
||||
child: new.to_replacement_node(old_children),
|
||||
},
|
||||
}),
|
||||
(Some(_), None) => patches.push(Patch {
|
||||
path: path.to_owned(),
|
||||
action: PatchAction::RemoveChild { at: a },
|
||||
}),
|
||||
(Some(old), Some(new)) if old != new => {
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
a += 1;
|
||||
}
|
||||
|
||||
// common suffix
|
||||
while b >= a {
|
||||
let old = old.get(b);
|
||||
let new = new.get(b);
|
||||
|
||||
match (old, new) {
|
||||
(None, Some(new)) => patches.push(Patch {
|
||||
path: path.to_owned(),
|
||||
action: PatchAction::InsertChildAfter {
|
||||
after: b - 1,
|
||||
child: new.to_replacement_node(old_children),
|
||||
},
|
||||
}),
|
||||
(Some(_), None) => patches.push(Patch {
|
||||
path: path.to_owned(),
|
||||
action: PatchAction::RemoveChild { at: b },
|
||||
}),
|
||||
(Some(old), Some(new)) if old != new => {
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if b == 0 {
|
||||
break;
|
||||
}
|
||||
b -= 1;
|
||||
}
|
||||
|
||||
// diffing in middle
|
||||
if b >= a {
|
||||
let old_slice_end =
|
||||
if b >= old.len() { old.len() - 1 } else { b };
|
||||
let new_slice_end =
|
||||
if b >= new.len() { new.len() - 1 } else { b };
|
||||
let old = &old[a..=old_slice_end];
|
||||
let new = &new[a..=new_slice_end];
|
||||
|
||||
for (new_idx, new_node) in new.iter().enumerate() {
|
||||
match old.get(new_idx) {
|
||||
Some(old_node) => {
|
||||
let mut new_path = path.to_vec();
|
||||
new_path.push(new_idx + a);
|
||||
let diffs = old_node.diff_at(
|
||||
new_node,
|
||||
&new_path,
|
||||
old_children,
|
||||
);
|
||||
patches.extend(&mut diffs.into_iter());
|
||||
}
|
||||
None => patches.push(Patch {
|
||||
while i > 0 || j > 0 {
|
||||
if i > 0 && j > 0 && old[i - 1] == new[j - 1] {
|
||||
i -= 1;
|
||||
j -= 1;
|
||||
} else {
|
||||
let current = mat[j * width + i];
|
||||
if i > 0
|
||||
&& j > 0
|
||||
&& mat[(j - 1) * width + i - 1] + 1 == current
|
||||
{
|
||||
let mut new_path = path.to_owned();
|
||||
new_path.push(i - 1);
|
||||
let diffs = old[i - 1].diff_at(
|
||||
&new[j - 1],
|
||||
&new_path,
|
||||
old_children,
|
||||
);
|
||||
patches.extend(&mut diffs.into_iter());
|
||||
i -= 1;
|
||||
j -= 1;
|
||||
} else if i > 0 && mat[j * width + i - 1] + 1 == current {
|
||||
patches.push(Patch {
|
||||
path: path.to_owned(),
|
||||
action: PatchAction::RemoveChild { at: i - 1 },
|
||||
});
|
||||
i -= 1;
|
||||
} else if j > 0 && mat[(j - 1) * width + i] + 1 == current {
|
||||
patches.push(Patch {
|
||||
path: path.to_owned(),
|
||||
action: PatchAction::InsertChild {
|
||||
before: new_idx,
|
||||
child: new_node
|
||||
before: i,
|
||||
child: new[j - 1]
|
||||
.to_replacement_node(old_children),
|
||||
},
|
||||
}),
|
||||
});
|
||||
j -= 1;
|
||||
} else {
|
||||
unreachable!();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -514,23 +488,17 @@ mod tests {
|
||||
let delta = a.diff(&b);
|
||||
assert_eq!(
|
||||
delta,
|
||||
vec![
|
||||
Patch {
|
||||
path: vec![],
|
||||
action: PatchAction::InsertChildAfter {
|
||||
after: 0,
|
||||
child: ReplacementNode::Element {
|
||||
name: "button".into(),
|
||||
attrs: vec![],
|
||||
children: vec![ReplacementNode::Html("bar".into())]
|
||||
}
|
||||
vec![Patch {
|
||||
path: vec![],
|
||||
action: PatchAction::InsertChild {
|
||||
before: 0,
|
||||
child: ReplacementNode::Element {
|
||||
name: "button".into(),
|
||||
attrs: vec![],
|
||||
children: vec![ReplacementNode::Html("foo".into())]
|
||||
}
|
||||
},
|
||||
Patch {
|
||||
path: vec![0, 0],
|
||||
action: PatchAction::SetText("foo".into())
|
||||
}
|
||||
]
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -121,6 +121,10 @@ impl ViewMacros {
|
||||
}
|
||||
diffs
|
||||
} else {
|
||||
// TODO: instead of simply returning no patches, when number of views differs,
|
||||
// we can compare views content to determine which views were shifted
|
||||
// or come up with another idea that will allow to send patches when views were shifted/removed/added
|
||||
lock.insert(path.clone(), new_views);
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,9 @@ impl LNode {
|
||||
LNode::parse_node(child, views)?;
|
||||
}
|
||||
}
|
||||
Node::RawText(text) => {
|
||||
views.push(LNode::Text(text.to_string_best()));
|
||||
}
|
||||
Node::Text(text) => {
|
||||
views.push(LNode::Text(text.value_string()));
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
console.log("[HOT RELOADING] Connected to server.\n\nNote: `cargo-leptos watch --hot-reload` only works with the `nightly` feature enabled on Leptos.");
|
||||
function patch(json) {
|
||||
try {
|
||||
const views = JSON.parse(json);
|
||||
for (const [id, patches] of views) {
|
||||
console.log("[HOT RELOAD]", id, patches);
|
||||
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT),
|
||||
const walker = document.createTreeWalker(
|
||||
document.body,
|
||||
NodeFilter.SHOW_COMMENT,
|
||||
),
|
||||
open = `hot-reload|${id}|open`,
|
||||
close = `hot-reload|${id}|close`;
|
||||
let start, end;
|
||||
@@ -21,150 +23,200 @@ function patch(json) {
|
||||
}
|
||||
|
||||
for (const [start, end] of instances) {
|
||||
// build tree of current actual children
|
||||
const actualChildren = childrenFromRange(start.parentElement, start, end);
|
||||
const actions = [];
|
||||
|
||||
// build up the set of actions
|
||||
for (const patch of patches) {
|
||||
const actualChildren = childrenFromRange(
|
||||
start.parentElement,
|
||||
start,
|
||||
end,
|
||||
);
|
||||
const child = childAtPath(
|
||||
actualChildren.length > 1 ? { children: actualChildren } : actualChildren[0],
|
||||
patch.path
|
||||
actualChildren.length > 1
|
||||
? { children: actualChildren }
|
||||
: actualChildren[0],
|
||||
patch.path,
|
||||
);
|
||||
const action = patch.action;
|
||||
if (action == "ClearChildren") {
|
||||
actions.push(() => {
|
||||
console.log("[HOT RELOAD] > ClearChildren", child.node);
|
||||
console.log("[HOT RELOAD] > ClearChildren", child.node);
|
||||
if (child.node) {
|
||||
child.node.textContent = "";
|
||||
});
|
||||
} else if (action.ReplaceWith) {
|
||||
actions.push(() => {
|
||||
console.log("[HOT RELOAD] > ReplaceWith", child, action.ReplaceWith);
|
||||
const replacement = fromReplacementNode(action.ReplaceWith, actualChildren);
|
||||
if (child.node) {
|
||||
child.node.replaceWith(replacement);
|
||||
} else {
|
||||
const range = new Range();
|
||||
range.setStartAfter(child.start);
|
||||
range.setEndAfter(child.end);
|
||||
range.deleteContents();
|
||||
child.start.replaceWith(replacement);
|
||||
} else {
|
||||
for (const existingChild of child.children) {
|
||||
let parent = existingChild.node.parentElement;
|
||||
parent.removeChild(existingChild.node);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (action.ReplaceWith) {
|
||||
console.log(
|
||||
"[HOT RELOAD] > ReplaceWith",
|
||||
child,
|
||||
action.ReplaceWith,
|
||||
);
|
||||
const replacement = fromReplacementNode(
|
||||
action.ReplaceWith,
|
||||
actualChildren,
|
||||
);
|
||||
if (child.node) {
|
||||
child.node.replaceWith(replacement);
|
||||
} else {
|
||||
if (child.children) {
|
||||
child.children[0].node.parentElement.insertBefore(
|
||||
replacement,
|
||||
child.children[0].node,
|
||||
);
|
||||
for (const existingChild of child.children) {
|
||||
existingChild.node.parentElement.removeChild(
|
||||
existingChild.node,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (action.ChangeTagName) {
|
||||
const oldNode = child.node;
|
||||
actions.push(() => {
|
||||
console.log("[HOT RELOAD] > ChangeTagName", child.node, action.ChangeTagName);
|
||||
const newElement = document.createElement(action.ChangeTagName);
|
||||
for (const attr of oldNode.attributes) {
|
||||
newElement.setAttribute(attr.name, attr.value);
|
||||
}
|
||||
for (const childNode of child.node.childNodes) {
|
||||
newElement.appendChild(childNode);
|
||||
}
|
||||
console.log(
|
||||
"[HOT RELOAD] > ChangeTagName",
|
||||
child.node,
|
||||
action.ChangeTagName,
|
||||
);
|
||||
const newElement = document.createElement(action.ChangeTagName);
|
||||
for (const attr of oldNode.attributes) {
|
||||
newElement.setAttribute(attr.name, attr.value);
|
||||
}
|
||||
for (const childNode of child.node.childNodes) {
|
||||
newElement.appendChild(childNode);
|
||||
}
|
||||
|
||||
child.node.replaceWith(newElement);
|
||||
});
|
||||
child.node.replaceWith(newElement);
|
||||
} else if (action.RemoveAttribute) {
|
||||
actions.push(() => {
|
||||
console.log("[HOT RELOAD] > RemoveAttribute", child.node, action.RemoveAttribute);
|
||||
child.node.removeAttribute(action.RemoveAttribute);
|
||||
});
|
||||
console.log(
|
||||
"[HOT RELOAD] > RemoveAttribute",
|
||||
child.node,
|
||||
action.RemoveAttribute,
|
||||
);
|
||||
child.node.removeAttribute(action.RemoveAttribute);
|
||||
} else if (action.SetAttribute) {
|
||||
const [name, value] = action.SetAttribute;
|
||||
actions.push(() => {
|
||||
console.log("[HOT RELOAD] > SetAttribute", child.node, action.SetAttribute);
|
||||
child.node.setAttribute(name, value);
|
||||
});
|
||||
console.log(
|
||||
"[HOT RELOAD] > SetAttribute",
|
||||
child.node,
|
||||
action.SetAttribute,
|
||||
);
|
||||
child.node.setAttribute(name, value);
|
||||
} else if (action.SetText) {
|
||||
const node = child.node;
|
||||
actions.push(() => {
|
||||
console.log("[HOT RELOAD] > SetText", child.node, action.SetText);
|
||||
node.textContent = action.SetText;
|
||||
});
|
||||
console.log("[HOT RELOAD] > SetText", child.node, action.SetText);
|
||||
node.textContent = action.SetText;
|
||||
} else if (action.AppendChildren) {
|
||||
actions.push(() => {
|
||||
console.log("[HOT RELOAD] > AppendChildren", child.node, action.AppendChildren);
|
||||
const newChildren = fromReplacementNode(action.AppendChildren, actualChildren);
|
||||
child.node.append(newChildren);
|
||||
});
|
||||
console.log(
|
||||
"[HOT RELOAD] > AppendChildren",
|
||||
child.node,
|
||||
action.AppendChildren,
|
||||
);
|
||||
const newChildren = action.AppendChildren.map((x) =>
|
||||
fromReplacementNode(x, actualChildren),
|
||||
);
|
||||
child.node.append(...newChildren);
|
||||
} else if (action.RemoveChild) {
|
||||
actions.push(() => {
|
||||
console.log("[HOT RELOAD] > RemoveChild", child.node, child.children, action.RemoveChild);
|
||||
const toRemove = child.children[action.RemoveChild.at];
|
||||
let toRemoveNode = toRemove.node;
|
||||
if (!toRemoveNode) {
|
||||
const range = new Range();
|
||||
range.setStartBefore(toRemove.start);
|
||||
range.setEndAfter(toRemove.end);
|
||||
toRemoveNode = range.deleteContents();
|
||||
} else {
|
||||
toRemoveNode.parentNode.removeChild(toRemoveNode);
|
||||
}
|
||||
});
|
||||
console.log(
|
||||
"[HOT RELOAD] > RemoveChild",
|
||||
child.node,
|
||||
child.children,
|
||||
action.RemoveChild,
|
||||
);
|
||||
const toRemove = child.children[action.RemoveChild.at];
|
||||
let toRemoveNode = toRemove.node;
|
||||
if (!toRemoveNode) {
|
||||
const range = new Range();
|
||||
range.setStartBefore(toRemove.start);
|
||||
range.setEndAfter(toRemove.end);
|
||||
toRemoveNode = range.deleteContents();
|
||||
} else {
|
||||
toRemoveNode.parentNode.removeChild(toRemoveNode);
|
||||
}
|
||||
} else if (action.InsertChild) {
|
||||
const newChild = fromReplacementNode(action.InsertChild.child, actualChildren);
|
||||
const newChild = fromReplacementNode(
|
||||
action.InsertChild.child,
|
||||
actualChildren,
|
||||
);
|
||||
let children = [];
|
||||
if (child.children) {
|
||||
children = child.children;
|
||||
} else if (child.start && child.end) {
|
||||
children = childrenFromRange(child.node || child.start.parentElement, start, end);
|
||||
children = childrenFromRange(
|
||||
child.node || child.start.parentElement,
|
||||
start,
|
||||
end,
|
||||
);
|
||||
} else {
|
||||
console.warn("InsertChildAfter could not build children.");
|
||||
}
|
||||
const before = children[action.InsertChild.before];
|
||||
actions.push(() => {
|
||||
console.log("[HOT RELOAD] > InsertChild", child, child.node, action.InsertChild, " before ", before);
|
||||
if (!before && child.node) {
|
||||
child.node.appendChild(newChild);
|
||||
} else {
|
||||
let node = child.node || child.end.parentElement;
|
||||
const reference = before ? before.node || before.start : child.end;
|
||||
node.insertBefore(newChild, reference);
|
||||
}
|
||||
});
|
||||
const beforeNode = children[action.InsertChild.before];
|
||||
console.log(
|
||||
"[HOT RELOAD] > InsertChild",
|
||||
child,
|
||||
child.node,
|
||||
action.InsertChild,
|
||||
" before ",
|
||||
beforeNode,
|
||||
);
|
||||
if (beforeNode) {
|
||||
let node = beforeNode.node || beforeNode.start.previousSibling;
|
||||
node.parentElement.insertBefore(newChild, node);
|
||||
} else if (child.node) {
|
||||
child.node.appendChild(newChild);
|
||||
} else if (children) {
|
||||
let lastNode = children[children.length - 1];
|
||||
let afterNode = lastNode.node || lastNode.end.nextSibling;
|
||||
afterNode.after(newChild);
|
||||
}
|
||||
} else if (action.InsertChildAfter) {
|
||||
const newChild = fromReplacementNode(action.InsertChildAfter.child, actualChildren);
|
||||
const newChild = fromReplacementNode(
|
||||
action.InsertChildAfter.child,
|
||||
actualChildren,
|
||||
);
|
||||
let children = [];
|
||||
if (child.children) {
|
||||
children = child.children;
|
||||
} else if (child.start && child.end) {
|
||||
children = childrenFromRange(child.node || child.start.parentElement, start, end);
|
||||
children = childrenFromRange(
|
||||
child.node || child.start.parentElement,
|
||||
start,
|
||||
end,
|
||||
);
|
||||
} else {
|
||||
console.warn("InsertChildAfter could not build children.");
|
||||
}
|
||||
const after = children[action.InsertChildAfter.after];
|
||||
actions.push(() => {
|
||||
console.log(
|
||||
"[HOT RELOAD] > InsertChildAfter",
|
||||
child,
|
||||
child.node,
|
||||
action.InsertChildAfter,
|
||||
" after ",
|
||||
after
|
||||
);
|
||||
if (child.node && (!after || !(after.node || after.start).nextSibling)) {
|
||||
child.node.appendChild(newChild);
|
||||
console.log(
|
||||
"[HOT RELOAD] > InsertChildAfter",
|
||||
child,
|
||||
child.node,
|
||||
action.InsertChildAfter,
|
||||
" after ",
|
||||
after,
|
||||
);
|
||||
if (
|
||||
child.node &&
|
||||
(!after || !(after.node || after.start).nextSibling)
|
||||
) {
|
||||
child.node.appendChild(newChild);
|
||||
} else {
|
||||
const node = child.node || child.end;
|
||||
const parent =
|
||||
node.nodeType === Node.COMMENT_NODE ? node.parentNode : node;
|
||||
if (!after) {
|
||||
parent.appendChild(newChild);
|
||||
} else {
|
||||
const node = child.node || child.end;
|
||||
const parent = node.nodeType === Node.COMMENT_NODE ? node.parentNode : node;
|
||||
if (!after) {
|
||||
parent.appendChild(newChild);
|
||||
} else {
|
||||
parent.insertBefore(newChild, (after.node || after.start).nextSibling);
|
||||
}
|
||||
parent.insertBefore(
|
||||
newChild,
|
||||
(after.node || after.start).nextSibling,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.warn("[HOT RELOADING] Unmatched action", action);
|
||||
}
|
||||
}
|
||||
|
||||
// actually run the actions
|
||||
// the reason we delay them is so that children aren't moved before other children are found, etc.
|
||||
for (const action of actions) {
|
||||
action();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -191,8 +243,10 @@ function patch(json) {
|
||||
return element;
|
||||
} else {
|
||||
const child = childAtPath(
|
||||
actualChildren.length > 1 ? { children: actualChildren } : actualChildren[0],
|
||||
node.Path
|
||||
actualChildren.length > 1
|
||||
? { children: actualChildren }
|
||||
: actualChildren[0],
|
||||
node.Path,
|
||||
);
|
||||
if (child) {
|
||||
let childNode = child.node;
|
||||
@@ -215,7 +269,10 @@ function patch(json) {
|
||||
}
|
||||
return childNode;
|
||||
} else {
|
||||
console.warn("[HOT RELOADING] Could not find replacement node at ", node.Path);
|
||||
console.warn(
|
||||
"[HOT RELOADING] Could not find replacement node at ",
|
||||
node.Path,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -227,13 +284,16 @@ function patch(json) {
|
||||
NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT,
|
||||
{
|
||||
acceptNode(node) {
|
||||
if (node.parentNode == element && (!range || range.isPointInRange(node, 0))) {
|
||||
if (
|
||||
node.parentNode == element &&
|
||||
(!range || range.isPointInRange(node, 0))
|
||||
) {
|
||||
return NodeFilter.FILTER_ACCEPT;
|
||||
} else {
|
||||
return NodeFilter.FILTER_REJECT;
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
const actualChildren = [],
|
||||
elementCount = {};
|
||||
@@ -259,18 +319,22 @@ function patch(json) {
|
||||
node: walker.currentNode,
|
||||
});
|
||||
} else if (walker.currentNode.nodeType == Node.COMMENT_NODE) {
|
||||
if (walker.currentNode.textContent.trim().startsWith("hot-reload")) {
|
||||
if (walker.currentNode.textContent.trim().endsWith("-children|open")) {
|
||||
if (walker.currentNode.textContent.trim().startsWith("hot-reload|")) {
|
||||
if (walker.currentNode.textContent.trim().endsWith("|open")) {
|
||||
const startingName = walker.currentNode.textContent.trim();
|
||||
const componentName = startingName.replace("-children|open").replace("hot-reload|");
|
||||
const endingName = `hot-reload|${componentName}-children|close`;
|
||||
const componentName = startingName
|
||||
.replace("|open", "")
|
||||
.replace("hot-reload|", "");
|
||||
const endingName = `hot-reload|${componentName}|close`;
|
||||
let start = walker.currentNode;
|
||||
let depth = 1;
|
||||
|
||||
while (walker.nextNode()) {
|
||||
if (walker.currentNode.textContent.trim() == endingName) {
|
||||
depth--;
|
||||
} else if (walker.currentNode.textContent.trim() == startingName) {
|
||||
} else if (
|
||||
walker.currentNode.textContent.trim() == startingName
|
||||
) {
|
||||
depth++;
|
||||
}
|
||||
|
||||
@@ -283,7 +347,11 @@ function patch(json) {
|
||||
type: "fragment",
|
||||
start: start.nextSibling,
|
||||
end: end.previousSibling,
|
||||
children: childrenFromRange(start.parentElement, start.nextSibling, end.previousSibling),
|
||||
children: childrenFromRange(
|
||||
start.parentElement,
|
||||
start.nextSibling,
|
||||
end.previousSibling,
|
||||
),
|
||||
});
|
||||
}
|
||||
} else if (walker.currentNode.textContent.trim() == "<() />") {
|
||||
@@ -358,7 +426,10 @@ function patch(json) {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.warn("[HOT RELOADING] Building children, encountered", walker.currentNode);
|
||||
console.warn(
|
||||
"[HOT RELOADING] Building children, encountered",
|
||||
walker.currentNode,
|
||||
);
|
||||
}
|
||||
}
|
||||
return actualChildren;
|
||||
@@ -374,7 +445,11 @@ function patch(json) {
|
||||
} else if (path == [0]) {
|
||||
return element;
|
||||
} else if (element.start && element.end) {
|
||||
const actualChildren = childrenFromRange(element.node || element.start.parentElement, element.start, element.end);
|
||||
const actualChildren = childrenFromRange(
|
||||
element.node || element.start.parentElement,
|
||||
element.start,
|
||||
element.end,
|
||||
);
|
||||
return childAtPath({ children: actualChildren }, path);
|
||||
} else {
|
||||
console.warn("[HOT RELOADING] Child at ", path, "not found in ", element);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user