mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 15:44:42 -05:00
Compare commits
190 Commits
pause-effe
...
0.8.0-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2faae43d5f | ||
|
|
6760c87e83 | ||
|
|
e19e42c650 | ||
|
|
fb4be49ebf | ||
|
|
0b4cbbc17d | ||
|
|
dbbeb7c6ef | ||
|
|
36aef2565d | ||
|
|
a2a7eb8a2a | ||
|
|
97e22e2506 | ||
|
|
8bedacb0c7 | ||
|
|
56b7b9a16a | ||
|
|
d04d4c77f9 | ||
|
|
5c75928b5b | ||
|
|
abc5631654 | ||
|
|
40e5288ac1 | ||
|
|
335934d40e | ||
|
|
6ee72f42e2 | ||
|
|
93af23a970 | ||
|
|
95e8ae84af | ||
|
|
5cfe7f6b5e | ||
|
|
0404efd5c3 | ||
|
|
93173c1400 | ||
|
|
cd2904f6a6 | ||
|
|
6b453845f9 | ||
|
|
111b84ce3b | ||
|
|
5633148047 | ||
|
|
4edb012de3 | ||
|
|
b2bea2e6b7 | ||
|
|
acbd6378a8 | ||
|
|
b7462aab10 | ||
|
|
7593540774 | ||
|
|
ed915f8e06 | ||
|
|
f65d87d566 | ||
|
|
5034539411 | ||
|
|
bc48aa4228 | ||
|
|
d2c81fe955 | ||
|
|
28eb96831a | ||
|
|
330920eae2 | ||
|
|
a94bc0a6da | ||
|
|
f85e01f4d6 | ||
|
|
599c87c88a | ||
|
|
3ca98279e1 | ||
|
|
a730bffe13 | ||
|
|
d0bf843821 | ||
|
|
7250bc312e | ||
|
|
0e8242f94c | ||
|
|
439b41f0e8 | ||
|
|
18570e970c | ||
|
|
787bf385d3 | ||
|
|
b6d2808671 | ||
|
|
2e4d94b6c6 | ||
|
|
66f9c8c999 | ||
|
|
352080d91a | ||
|
|
1e579614a5 | ||
|
|
a4e47d4086 | ||
|
|
3164721fdb | ||
|
|
fee4bccb32 | ||
|
|
4ba9f67440 | ||
|
|
2242ad1847 | ||
|
|
131b18bddb | ||
|
|
5e9d6e2dfd | ||
|
|
d76e5bb4ea | ||
|
|
f752e32ae3 | ||
|
|
a9197102a6 | ||
|
|
58f1bf95e1 | ||
|
|
d84ab6d9bf | ||
|
|
f069d4478e | ||
|
|
65b5d55d62 | ||
|
|
860ad7a221 | ||
|
|
901e038aa0 | ||
|
|
f49f0965bc | ||
|
|
bb62d08d3f | ||
|
|
bfe04593fd | ||
|
|
38a3aae28e | ||
|
|
5eaaff045f | ||
|
|
5b484eaec4 | ||
|
|
e9384e3286 | ||
|
|
5149ad54db | ||
|
|
4d9ec54ad1 | ||
|
|
a1cd7ae9a1 | ||
|
|
083f9c663f | ||
|
|
63c9549120 | ||
|
|
6232f6482a | ||
|
|
825e89f25c | ||
|
|
3dbb251853 | ||
|
|
98e00fcb3b | ||
|
|
5da4c438d9 | ||
|
|
80ed74c075 | ||
|
|
cdee2a9476 | ||
|
|
c97ab9a72c | ||
|
|
4fc8972f2b | ||
|
|
79e9340a9b | ||
|
|
41d01cedb2 | ||
|
|
b800c009c7 | ||
|
|
374a020d84 | ||
|
|
83fcf8663c | ||
|
|
e7a73595de | ||
|
|
a9a988e0e1 | ||
|
|
db10d961df | ||
|
|
fb608158cb | ||
|
|
1a472ebad1 | ||
|
|
2d1b66a5c6 | ||
|
|
c524b0aefc | ||
|
|
e4c977911c | ||
|
|
f488d4b5b7 | ||
|
|
d4cfd0e2cb | ||
|
|
a4b0d3408c | ||
|
|
2037bf12cb | ||
|
|
2747a496fc | ||
|
|
c286812116 | ||
|
|
1e0a9ef189 | ||
|
|
1363b941bc | ||
|
|
7479010f84 | ||
|
|
f7a1a2cab2 | ||
|
|
92b82688a6 | ||
|
|
9b9983af79 | ||
|
|
04e79a0dc4 | ||
|
|
f64951126e | ||
|
|
0a29071779 | ||
|
|
efcb6f6d21 | ||
|
|
42988b1bc1 | ||
|
|
6904eec207 | ||
|
|
7eb8ca702d | ||
|
|
c75397ea75 | ||
|
|
848fd724dd | ||
|
|
5bc254d49f | ||
|
|
885f4a1654 | ||
|
|
ddd243d07a | ||
|
|
362c300eac | ||
|
|
284a724e5f | ||
|
|
1a7b40b507 | ||
|
|
73dd677843 | ||
|
|
131f414fdc | ||
|
|
c3ed874d4d | ||
|
|
f003e50446 | ||
|
|
ea29685c92 | ||
|
|
6ecc681cdd | ||
|
|
49e44a2ec2 | ||
|
|
7c34b4a4a5 | ||
|
|
7157958822 | ||
|
|
37cf25fba5 | ||
|
|
f975b8d33b | ||
|
|
d37450d12f | ||
|
|
4804dac32d | ||
|
|
a9f27d6128 | ||
|
|
04cb036a7d | ||
|
|
1d3784ed7b | ||
|
|
8cc1a34c00 | ||
|
|
68f4d46c5f | ||
|
|
590728e47e | ||
|
|
e84b527743 | ||
|
|
96b125d54f | ||
|
|
16d66362f8 | ||
|
|
e27801a2c2 | ||
|
|
b81f71997b | ||
|
|
2a11325749 | ||
|
|
5604f3e979 | ||
|
|
3a9a0891a3 | ||
|
|
a39add50c0 | ||
|
|
2a2675dd36 | ||
|
|
6ad300c592 | ||
|
|
b4e683d969 | ||
|
|
c2289b23a7 | ||
|
|
299acd25f3 | ||
|
|
287fc47163 | ||
|
|
8f74a6d8a0 | ||
|
|
597175a54b | ||
|
|
ede25b9e3d | ||
|
|
8f636e354a | ||
|
|
7da64f22c4 | ||
|
|
0073ae7d8a | ||
|
|
8465716a19 | ||
|
|
0e24b2e63f | ||
|
|
c64d205984 | ||
|
|
f17cb98eb0 | ||
|
|
30f3e82664 | ||
|
|
152d5a5c92 | ||
|
|
669e1ba7fa | ||
|
|
2ad6a086f9 | ||
|
|
32e58d6b66 | ||
|
|
a107443104 | ||
|
|
c859b07901 | ||
|
|
a9868bea2b | ||
|
|
7183c2b993 | ||
|
|
7a03621db1 | ||
|
|
2b589fa61f | ||
|
|
35e6f17930 | ||
|
|
d1513a4a0b | ||
|
|
aa27b9e474 | ||
|
|
cfe925d58f |
1
.github/workflows/ci-changed-examples.yml
vendored
1
.github/workflows/ci-changed-examples.yml
vendored
@@ -28,5 +28,6 @@ jobs:
|
||||
uses: ./.github/workflows/run-cargo-make-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
erased_mode: ${{ matrix.erased_mode }}
|
||||
cargo_make_task: "ci"
|
||||
toolchain: stable
|
||||
|
||||
1
.github/workflows/ci-examples.yml
vendored
1
.github/workflows/ci-examples.yml
vendored
@@ -25,5 +25,6 @@ jobs:
|
||||
uses: ./.github/workflows/run-cargo-make-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
erased_mode: ${{ matrix.erased_mode }}
|
||||
cargo_make_task: "ci"
|
||||
toolchain: stable
|
||||
|
||||
4
.github/workflows/ci-semver.yml
vendored
4
.github/workflows/ci-semver.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
test:
|
||||
needs: [get-leptos-changed]
|
||||
if: needs.get-leptos-changed.outputs.leptos_changed == 'true' && github.event.pull_request.labels[0].name != 'breaking'
|
||||
name: Run semver check (nightly-2024-08-01)
|
||||
name: Run semver check (nightly-2025-03-05)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install Glib
|
||||
@@ -30,4 +30,4 @@ jobs:
|
||||
- name: Semver Checks
|
||||
uses: obi1kenobi/cargo-semver-checks-action@v2
|
||||
with:
|
||||
rust-toolchain: nightly-2024-08-01
|
||||
rust-toolchain: nightly-2025-03-05
|
||||
|
||||
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -25,5 +25,6 @@ jobs:
|
||||
uses: ./.github/workflows/run-cargo-make-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
erased_mode: ${{ matrix.erased_mode }}
|
||||
cargo_make_task: "ci"
|
||||
toolchain: nightly-2024-08-01
|
||||
toolchain: nightly-2025-03-05
|
||||
|
||||
@@ -50,5 +50,5 @@ jobs:
|
||||
echo "matrix={\"directory\":${{ steps.changed-dirs.outputs.all_changed_files }}}" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
# Create matrix with one item to prevent an empty vector error
|
||||
echo "matrix={\"directory\":[\"NO_CHANGE\"]}" >> "$GITHUB_OUTPUT"
|
||||
echo "matrix={\"directory\":[\"NO_CHANGE\"], \"erased_mode\": [false, true]}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
2
.github/workflows/get-examples-matrix.yml
vendored
2
.github/workflows/get-examples-matrix.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
sed 's/\/$//' |
|
||||
jq -R -s -c 'split("\n")[:-1]')
|
||||
echo "Example Directories: $examples"
|
||||
echo "matrix={\"directory\":$examples}" >> "$GITHUB_OUTPUT"
|
||||
echo "matrix={\"directory\":$examples, \"erased_mode\": [false, true]}" >> "$GITHUB_OUTPUT"
|
||||
- name: Print Location Info
|
||||
run: |
|
||||
echo "Workspace: ${{ github.workspace }}"
|
||||
|
||||
2
.github/workflows/get-leptos-matrix.yml
vendored
2
.github/workflows/get-leptos-matrix.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
sed "s|$(pwd)/||" |
|
||||
jq -R -s -c 'split("\n")[:-1]')
|
||||
echo "Leptos Directories: $crates"
|
||||
echo "matrix={\"directory\":$crates}" >> "$GITHUB_OUTPUT"
|
||||
echo "matrix={\"directory\":$crates, \"erased_mode\": [false, true]}" >> "$GITHUB_OUTPUT"
|
||||
- name: Print Location Info
|
||||
run: |
|
||||
echo "Workspace: ${{ github.workspace }}"
|
||||
|
||||
6
.github/workflows/run-cargo-make-task.yml
vendored
6
.github/workflows/run-cargo-make-task.yml
vendored
@@ -5,6 +5,9 @@ on:
|
||||
directory:
|
||||
required: true
|
||||
type: string
|
||||
erased_mode:
|
||||
required: true
|
||||
type: boolean
|
||||
cargo_make_task:
|
||||
required: true
|
||||
type: string
|
||||
@@ -15,9 +18,10 @@ env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
RUSTFLAGS: ${{ inputs.erased_mode && '--cfg erase_components' || '' }}
|
||||
jobs:
|
||||
test:
|
||||
name: Run ${{ inputs.cargo_make_task }} (${{ inputs.toolchain }})
|
||||
name: "Run ${{ inputs.cargo_make_task }} (${{ inputs.toolchain }}) (erased_mode: ${{ inputs.erased_mode }})"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Free Disk Space
|
||||
|
||||
1105
Cargo.lock
generated
1105
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
45
Cargo.toml
45
Cargo.toml
@@ -40,36 +40,39 @@ members = [
|
||||
exclude = ["benchmarks", "examples", "projects"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.7.7"
|
||||
version = "0.8.0-beta"
|
||||
edition = "2021"
|
||||
rust-version = "1.76"
|
||||
|
||||
[workspace.dependencies]
|
||||
throw_error = { path = "./any_error/", version = "0.2.0" }
|
||||
throw_error = { path = "./any_error/", version = "0.3.0" }
|
||||
any_spawner = { path = "./any_spawner/", version = "0.2.0" }
|
||||
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1" }
|
||||
either_of = { path = "./either_of/", version = "0.1.0" }
|
||||
hydration_context = { path = "./hydration_context", version = "0.2.0" }
|
||||
leptos = { path = "./leptos", version = "0.7.7" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.7.7" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.7.7" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.7" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.7" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.7.7" }
|
||||
leptos_router = { path = "./router", version = "0.7.7" }
|
||||
leptos_router_macro = { path = "./router_macro", version = "0.7.7" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.7.7" }
|
||||
leptos_meta = { path = "./meta", version = "0.7.7" }
|
||||
either_of = { path = "./either_of/", version = "0.1.5" }
|
||||
hydration_context = { path = "./hydration_context", version = "0.3.0" }
|
||||
itertools = "0.14.0"
|
||||
leptos = { path = "./leptos", version = "0.8.0-beta" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.8.0-beta" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.8.0-beta" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.0-beta" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.8.0-beta" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.8.0-beta" }
|
||||
leptos_router = { path = "./router", version = "0.8.0-beta" }
|
||||
leptos_router_macro = { path = "./router_macro", version = "0.8.0-beta" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.8.0-beta" }
|
||||
leptos_meta = { path = "./meta", version = "0.8.0-beta" }
|
||||
next_tuple = { path = "./next_tuple", version = "0.1.0" }
|
||||
oco_ref = { path = "./oco", version = "0.2.0" }
|
||||
or_poisoned = { path = "./or_poisoned", version = "0.1.0" }
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.1.7" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.1.7" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.7" }
|
||||
server_fn = { path = "./server_fn", version = "0.7.7" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.7.7" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.7" }
|
||||
tachys = { path = "./tachys", version = "0.1.7" }
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.2.0-beta" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.2.0-beta" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.2.0-beta" }
|
||||
serde_json = "1.0.0"
|
||||
server_fn = { path = "./server_fn", version = "0.8.0-beta" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.8.0-beta" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.0-beta" }
|
||||
tachys = { path = "./tachys", version = "0.2.0-beta" }
|
||||
wasm-bindgen = { version = "0.2.100" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -21,7 +21,7 @@ use leptos::*;
|
||||
#[component]
|
||||
pub fn SimpleCounter(initial_value: i32) -> impl IntoView {
|
||||
// create a reactive signal with the initial value
|
||||
let (value, set_value) = create_signal(initial_value);
|
||||
let (value, set_value) = signal(initial_value);
|
||||
|
||||
// create event handlers for our buttons
|
||||
// note that `value` and `set_value` are `Copy`, so it's super easy to move them into closures
|
||||
@@ -46,7 +46,7 @@ pub fn SimpleCounter(initial_value: i32) -> impl IntoView {
|
||||
pub fn SimpleCounterWithBuilder(initial_value: i32) -> impl IntoView {
|
||||
use leptos::html::*;
|
||||
|
||||
let (value, set_value) = create_signal(initial_value);
|
||||
let (value, set_value) = signal(initial_value);
|
||||
let clear = move |_| set_value(0);
|
||||
let decrement = move |_| set_value.update(|value| *value -= 1);
|
||||
let increment = move |_| set_value.update(|value| *value += 1);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "throw_error"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
|
||||
@@ -9,7 +9,7 @@ use std::{
|
||||
error,
|
||||
fmt::{self, Display},
|
||||
future::Future,
|
||||
mem, ops,
|
||||
ops,
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
task::{Context, Poll},
|
||||
@@ -17,11 +17,6 @@ use std::{
|
||||
|
||||
/* Wrapper Types */
|
||||
|
||||
/// This is a result type into which any error can be converted.
|
||||
///
|
||||
/// Results are stored as [`Error`].
|
||||
pub type Result<T, E = Error> = core::result::Result<T, E>;
|
||||
|
||||
/// A generic wrapper for any error.
|
||||
#[derive(Debug, Clone)]
|
||||
#[repr(transparent)]
|
||||
@@ -109,7 +104,7 @@ pub fn get_error_hook() -> Option<Arc<dyn ErrorHook>> {
|
||||
/// Sets the current thread-local error hook, which will be invoked when [`throw`] is called.
|
||||
pub fn set_error_hook(hook: Arc<dyn ErrorHook>) -> ResetErrorHookOnDrop {
|
||||
ResetErrorHookOnDrop(
|
||||
ERROR_HOOK.with_borrow_mut(|this| mem::replace(this, Some(hook))),
|
||||
ERROR_HOOK.with_borrow_mut(|this| Option::replace(this, hook)),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ tokio = { version = "1.41", optional = true, default-features = false, features
|
||||
"rt",
|
||||
] }
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
wasm-bindgen-futures = { version = "0.4.47", optional = true }
|
||||
wasm-bindgen-futures = { version = "0.4.50", optional = true }
|
||||
|
||||
[features]
|
||||
async-executor = ["dep:async-executor"]
|
||||
|
||||
@@ -23,7 +23,7 @@ tokio-test = "0.4.0"
|
||||
miniserde = "0.1.0"
|
||||
gloo = "0.8.0"
|
||||
uuid = { version = "1.0", features = ["serde", "v4", "wasm-bindgen"] }
|
||||
wasm-bindgen = "0.2.0"
|
||||
wasm-bindgen = "0.2.100"
|
||||
lazy_static = "1.0"
|
||||
log = "0.4.0"
|
||||
strum = "0.24.0"
|
||||
|
||||
@@ -7,7 +7,7 @@ edition = "2021"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
console_error_panic_hook = "0.1.7"
|
||||
console_log = "1.0"
|
||||
gloo-utils = "0.2.0"
|
||||
@@ -20,18 +20,27 @@ leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_router = { path = "../../router" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "1.39", features = [ "rt-multi-thread", "macros", "time" ], optional = true }
|
||||
tokio = { version = "1.39", features = [
|
||||
"rt-multi-thread",
|
||||
"macros",
|
||||
"time",
|
||||
], optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
wasm-bindgen = "0.2.92"
|
||||
web-sys = { version = "0.3.69", features = [ "AddEventListenerOptions", "Document", "Element", "Event", "EventListener", "EventTarget", "Performance", "Window" ], optional = true }
|
||||
web-sys = { version = "0.3.69", features = [
|
||||
"AddEventListenerOptions",
|
||||
"Document",
|
||||
"Element",
|
||||
"Event",
|
||||
"EventListener",
|
||||
"EventTarget",
|
||||
"Performance",
|
||||
"Window",
|
||||
], optional = true }
|
||||
|
||||
[features]
|
||||
hydrate = [
|
||||
"leptos/hydrate",
|
||||
"dep:js-sys",
|
||||
"dep:web-sys",
|
||||
]
|
||||
hydrate = ["leptos/hydrate", "dep:js-sys", "dep:web-sys"]
|
||||
ssr = [
|
||||
"dep:axum",
|
||||
"dep:http-body-util",
|
||||
|
||||
@@ -13,7 +13,7 @@ leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_meta = { path = "../../meta" }
|
||||
leptos_router = { path = "../../router" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
|
||||
@@ -45,7 +45,7 @@ async fn main() {
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/special/:id", get(custom_handler))
|
||||
.route("/special/{id}", get(custom_handler))
|
||||
.leptos_routes(&leptos_options, routes, {
|
||||
let leptos_options = leptos_options.clone();
|
||||
move || shell(leptos_options.clone())
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use leptos::prelude::*;
|
||||
use leptos::tachys::html::style::style;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -16,7 +15,7 @@ pub enum CatError {
|
||||
|
||||
type CatCount = usize;
|
||||
|
||||
async fn fetch_cats(count: CatCount) -> Result<Vec<String>> {
|
||||
async fn fetch_cats(count: CatCount) -> Result<Vec<String>, Error> {
|
||||
if count > 0 {
|
||||
gloo_timers::future::TimeoutFuture::new(1000).await;
|
||||
// make the request
|
||||
@@ -42,11 +41,7 @@ async fn fetch_cats(count: CatCount) -> Result<Vec<String>> {
|
||||
pub fn fetch_example() -> impl IntoView {
|
||||
let (cat_count, set_cat_count) = signal::<CatCount>(1);
|
||||
|
||||
// we use new_unsync here because the reqwasm request type isn't Send
|
||||
// if we were doing SSR, then
|
||||
// 1) we'd want to use a Resource, so the data would be serialized to the client
|
||||
// 2) we'd need to make sure there was a thread-local spawner set up
|
||||
let cats = AsyncDerived::new_unsync(move || fetch_cats(cat_count.get()));
|
||||
let cats = LocalResource::new(move || fetch_cats(cat_count.get()));
|
||||
|
||||
let fallback = move |errors: ArcRwSignal<Errors>| {
|
||||
let error_list = move || {
|
||||
@@ -66,8 +61,6 @@ pub fn fetch_example() -> impl IntoView {
|
||||
}
|
||||
};
|
||||
|
||||
let spreadable = style(("background-color", "AliceBlue"));
|
||||
|
||||
view! {
|
||||
<div>
|
||||
<label>
|
||||
@@ -82,7 +75,7 @@ pub fn fetch_example() -> impl IntoView {
|
||||
/>
|
||||
|
||||
</label>
|
||||
<Transition fallback=|| view! { <div>"Loading..."</div> } {..spreadable}>
|
||||
<Transition fallback=|| view! { <div>"Loading..."</div> }>
|
||||
<ErrorBoundary fallback>
|
||||
<ul>
|
||||
{move || Suspend::new(async move {
|
||||
@@ -92,7 +85,7 @@ pub fn fetch_example() -> impl IntoView {
|
||||
.map(|s| {
|
||||
view! {
|
||||
<li>
|
||||
<img src=s.clone()/>
|
||||
<img src=s.clone() />
|
||||
</li>
|
||||
}
|
||||
})
|
||||
|
||||
@@ -20,7 +20,7 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
tracing = "0.1.40"
|
||||
gloo-net = { version = "0.6.0", features = ["http"] }
|
||||
reqwest = { version = "0.12.5", features = ["json"] }
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
|
||||
@@ -20,7 +20,7 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
tracing = "0.1.40"
|
||||
gloo-net = { version = "0.6.0", features = ["http"] }
|
||||
reqwest = { version = "0.12.5", features = ["json"] }
|
||||
axum = { version = "0.7.5", optional = true, features = ["http2"] }
|
||||
axum = { version = "0.8.1", optional = true, features = ["http2"] }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = [
|
||||
"fs",
|
||||
|
||||
@@ -23,7 +23,7 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
tracing = "0.1.40"
|
||||
gloo-net = { version = "0.6.0", features = ["http"] }
|
||||
reqwest = { version = "0.12.5", features = ["json"] }
|
||||
axum = { version = "0.7.5", default-features = false, optional = true }
|
||||
axum = { version = "0.8.1", default-features = false, optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
http = { version = "1.1", optional = true }
|
||||
web-sys = { version = "0.3.70", features = [
|
||||
|
||||
@@ -10,15 +10,12 @@ crate-type = ["cdylib", "rlib"]
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.30"
|
||||
http = "1.1"
|
||||
leptos = { path = "../../leptos", features = [
|
||||
"tracing",
|
||||
"islands",
|
||||
] }
|
||||
leptos = { path = "../../leptos", features = ["tracing", "islands"] }
|
||||
server_fn = { path = "../../server_fn", features = ["serde-lite"] }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
log = "0.4.22"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
|
||||
@@ -10,22 +10,20 @@ crate-type = ["cdylib", "rlib"]
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.30"
|
||||
http = "1.1"
|
||||
leptos = { path = "../../leptos", features = [
|
||||
"tracing",
|
||||
"islands",
|
||||
] }
|
||||
leptos = { path = "../../leptos", features = ["tracing", "islands"] }
|
||||
leptos_router = { path = "../../router" }
|
||||
server_fn = { path = "../../server_fn", features = ["serde-lite"] }
|
||||
leptos_axum = { path = "../../integrations/axum", features = [
|
||||
"dont-use-islands-router",
|
||||
"islands-router",
|
||||
], optional = true }
|
||||
log = "0.4.22"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
wasm-bindgen = "0.2.93"
|
||||
wasm-bindgen = "0.2.100"
|
||||
serde_json = "1.0.133"
|
||||
|
||||
[features]
|
||||
hydrate = ["leptos/hydrate"]
|
||||
@@ -58,11 +56,11 @@ site-root = "target/site"
|
||||
# Defaults to pkg
|
||||
site-pkg-dir = "pkg"
|
||||
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
|
||||
style-file = "./style.css"
|
||||
style-file = "style.css"
|
||||
# [Optional] Files in the asset-dir will be copied to the site-root directory
|
||||
assets-dir = "public"
|
||||
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
|
||||
site-addr = "127.0.0.1:3000"
|
||||
site-addr = "127.0.0.1:3009"
|
||||
# The port to use for automatic reload monitoring
|
||||
reload-port = 3001
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
|
||||
2852
examples/islands_router/mock_data.json
Normal file
2852
examples/islands_router/mock_data.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,140 +0,0 @@
|
||||
window.addEventListener("click", async (ev) => {
|
||||
// confirm that this is an <a> that meets our requirements
|
||||
if (
|
||||
ev.defaultPrevented ||
|
||||
ev.button !== 0 ||
|
||||
ev.metaKey ||
|
||||
ev.altKey ||
|
||||
ev.ctrlKey ||
|
||||
ev.shiftKey
|
||||
)
|
||||
return;
|
||||
|
||||
/** @type HTMLAnchorElement | undefined;*/
|
||||
const a = ev
|
||||
.composedPath()
|
||||
.find(el => el instanceof Node && el.nodeName.toUpperCase() === "A");
|
||||
|
||||
if (!a) return;
|
||||
|
||||
const svg = a.namespaceURI === "http://www.w3.org/2000/svg";
|
||||
const href = svg ? a.href.baseVal : a.href;
|
||||
const target = svg ? a.target.baseVal : a.target;
|
||||
if (target || (!href && !a.hasAttribute("state"))) return;
|
||||
|
||||
const rel = (a.getAttribute("rel") || "").split(/\s+/);
|
||||
if (a.hasAttribute("download") || (rel && rel.includes("external"))) return;
|
||||
|
||||
const url = svg ? new URL(href, document.baseURI) : new URL(href);
|
||||
if (
|
||||
url.origin !== window.location.origin // ||
|
||||
// TODO base
|
||||
//(basePath && url.pathname && !url.pathname.toLowerCase().startsWith(basePath.toLowerCase()))
|
||||
)
|
||||
return;
|
||||
|
||||
ev.preventDefault();
|
||||
|
||||
// fetch the new page
|
||||
const resp = await fetch(url);
|
||||
const htmlString = await resp.text();
|
||||
|
||||
// Use DOMParser to parse the HTML string
|
||||
const parser = new DOMParser();
|
||||
// TODO parse from the request stream instead?
|
||||
const doc = parser.parseFromString(htmlString, 'text/html');
|
||||
|
||||
// The 'doc' variable now contains the parsed DOM
|
||||
const transition = async () => {
|
||||
const oldDocWalker = document.createTreeWalker(document);
|
||||
const newDocWalker = doc.createTreeWalker(doc);
|
||||
let oldNode = oldDocWalker.currentNode;
|
||||
let newNode = newDocWalker.currentNode;
|
||||
while(oldDocWalker.nextNode() && newDocWalker.nextNode()) {
|
||||
oldNode = oldDocWalker.currentNode;
|
||||
newNode = newDocWalker.currentNode;
|
||||
// if the nodes are different, we need to replace the old with the new
|
||||
// because of the typed view tree, this should never actually happen
|
||||
if (oldNode.nodeType !== newNode.nodeType) {
|
||||
oldNode.replaceWith(newNode);
|
||||
}
|
||||
// if it's a text node, just update the text with the new text
|
||||
else if (oldNode.nodeType === Node.TEXT_NODE) {
|
||||
oldNode.textContent = newNode.textContent;
|
||||
}
|
||||
// if it's an element, replace if it's a different tag, or update attributes
|
||||
else if (oldNode.nodeType === Node.ELEMENT_NODE) {
|
||||
/** @type Element */
|
||||
const oldEl = oldNode;
|
||||
/** @type Element */
|
||||
const newEl = newNode;
|
||||
if (oldEl.tagName !== newEl.tagName) {
|
||||
oldEl.replaceWith(newEl);
|
||||
}
|
||||
else {
|
||||
for(const attr of newEl.attributes) {
|
||||
oldEl.setAttribute(attr.name, attr.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
// we use comment "branch marker" nodes to distinguish between different branches in the statically-typed view tree
|
||||
// if one of these marker is hit, then there are two options
|
||||
// 1) it's the same branch, and we just keep walking until the end
|
||||
// 2) it's a different branch, in which case the old can be replaced with the new wholesale
|
||||
else if (oldNode.nodeType === Node.COMMENT_NODE) {
|
||||
const oldText = oldNode.textContent;
|
||||
const newText = newNode.textContent;
|
||||
if(oldText.startsWith("bo") && newText !== oldText) {
|
||||
oldDocWalker.nextNode();
|
||||
newDocWalker.nextNode();
|
||||
const oldRange = new Range();
|
||||
const newRange = new Range();
|
||||
let oldBranches = 1;
|
||||
let newBranches = 1;
|
||||
while(oldBranches > 0 && newBranches > 0) {
|
||||
if(oldDocWalker.nextNode() && newDocWalker.nextNode()) {
|
||||
console.log(oldDocWalker.currentNode, newDocWalker.currentNode);
|
||||
if(oldDocWalker.currentNode.nodeType === Node.COMMENT_NODE) {
|
||||
if(oldDocWalker.currentNode.textContent.startsWith("bo")) {
|
||||
oldBranches += 1;
|
||||
} else if(oldDocWalker.currentNode.textContent.startsWith("bc")) {
|
||||
|
||||
oldBranches -= 1;
|
||||
}
|
||||
}
|
||||
if(newDocWalker.currentNode.nodeType === Node.COMMENT_NODE) {
|
||||
if(newDocWalker.currentNode.textContent.startsWith("bo")) {
|
||||
newBranches += 1;
|
||||
} else if(newDocWalker.currentNode.textContent.startsWith("bc")) {
|
||||
|
||||
newBranches -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
oldRange.setStartAfter(oldNode);
|
||||
oldRange.setEndBefore(oldDocWalker.currentNode);
|
||||
newRange.setStartAfter(newNode);
|
||||
newRange.setEndBefore(newDocWalker.currentNode);
|
||||
const newContents = newRange.extractContents();
|
||||
oldRange.deleteContents();
|
||||
oldRange.insertNode(newContents);
|
||||
oldNode.replaceWith(newNode);
|
||||
oldDocWalker.currentNode.replaceWith(newDocWalker.currentNode);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
} }
|
||||
}
|
||||
};
|
||||
// Not all browsers support startViewTransition; see https://caniuse.com/?search=startViewTransition
|
||||
if (document.startViewTransition) {
|
||||
await document.startViewTransition(transition);
|
||||
} else {
|
||||
await transition()
|
||||
}
|
||||
window.history.pushState(undefined, null, url);
|
||||
});
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
use leptos::prelude::*;
|
||||
use leptos_router::{
|
||||
components::{FlatRoutes, Route, Router},
|
||||
StaticSegment,
|
||||
use leptos::{
|
||||
either::{Either, EitherOf3},
|
||||
prelude::*,
|
||||
};
|
||||
use leptos_router::{
|
||||
components::{Route, Router, Routes},
|
||||
hooks::{use_params_map, use_query_map},
|
||||
path,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
view! {
|
||||
@@ -12,7 +17,7 @@ pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<AutoReload options=options.clone()/>
|
||||
<HydrationScripts options=options islands=true/>
|
||||
<HydrationScripts options=options islands=true islands_router=true/>
|
||||
<link rel="stylesheet" id="leptos" href="/pkg/islands.css"/>
|
||||
<link rel="shortcut icon" type="image/ico" href="/favicon.ico"/>
|
||||
</head>
|
||||
@@ -26,34 +31,180 @@ pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
view! {
|
||||
<script src="/routing.js"></script>
|
||||
<Router>
|
||||
<header>
|
||||
<h1>"My Application"</h1>
|
||||
<h1>"My Contacts"</h1>
|
||||
</header>
|
||||
<nav>
|
||||
<a href="/">"Page A"</a>
|
||||
<a href="/b">"Page B"</a>
|
||||
<a href="/">"Home"</a>
|
||||
<a href="/about">"About"</a>
|
||||
</nav>
|
||||
<main>
|
||||
<p>
|
||||
<label>"Home Checkbox" <input type="checkbox"/></label>
|
||||
</p>
|
||||
<FlatRoutes fallback=|| "Not found.">
|
||||
<Route path=StaticSegment("") view=PageA/>
|
||||
<Route path=StaticSegment("b") view=PageB/>
|
||||
</FlatRoutes>
|
||||
<Routes fallback=|| "Not found.">
|
||||
<Route path=path!("") view=Home/>
|
||||
<Route path=path!("user/:id") view=Details/>
|
||||
<Route path=path!("about") view=About/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn PageA() -> impl IntoView {
|
||||
view! { <label>"Page A" <input type="checkbox"/></label> }
|
||||
#[server]
|
||||
pub async fn search(query: String) -> Result<Vec<User>, ServerFnError> {
|
||||
let users = tokio::fs::read_to_string("./mock_data.json").await?;
|
||||
let data: Vec<User> = serde_json::from_str(&users)?;
|
||||
let query = query.to_ascii_lowercase();
|
||||
Ok(data
|
||||
.into_iter()
|
||||
.filter(|user| {
|
||||
user.first_name.to_ascii_lowercase().contains(&query)
|
||||
|| user.last_name.to_ascii_lowercase().contains(&query)
|
||||
|| user.email.to_ascii_lowercase().contains(&query)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn delete_user(id: u32) -> Result<(), ServerFnError> {
|
||||
let users = tokio::fs::read_to_string("./mock_data.json").await?;
|
||||
let mut data: Vec<User> = serde_json::from_str(&users)?;
|
||||
data.retain(|user| user.id != id);
|
||||
let new_json = serde_json::to_string(&data)?;
|
||||
tokio::fs::write("./mock_data.json", &new_json).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct User {
|
||||
id: u32,
|
||||
first_name: String,
|
||||
last_name: String,
|
||||
email: String,
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn PageB() -> impl IntoView {
|
||||
view! { <label>"Page B" <input type="checkbox"/></label> }
|
||||
pub fn Home() -> impl IntoView {
|
||||
let q = use_query_map();
|
||||
let q = move || q.read().get("q");
|
||||
let data = Resource::new(q, |q| async move {
|
||||
if let Some(q) = q {
|
||||
search(q).await
|
||||
} else {
|
||||
Ok(vec![])
|
||||
}
|
||||
});
|
||||
let delete_user_action = ServerAction::<DeleteUser>::new();
|
||||
|
||||
let view = move || {
|
||||
Suspend::new(async move {
|
||||
let users = data.await.unwrap();
|
||||
if q().is_none() {
|
||||
EitherOf3::A(view! {
|
||||
<p class="note">"Enter a search to begin viewing contacts."</p>
|
||||
})
|
||||
} else if users.is_empty() {
|
||||
EitherOf3::B(view! {
|
||||
<p class="note">"No users found matching that search."</p>
|
||||
})
|
||||
} else {
|
||||
EitherOf3::C(view! {
|
||||
<table>
|
||||
<tbody>
|
||||
<For
|
||||
each=move || users.clone()
|
||||
key=|user| user.id
|
||||
let:user
|
||||
>
|
||||
<tr>
|
||||
<td>{user.first_name}</td>
|
||||
<td>{user.last_name}</td>
|
||||
<td>{user.email}</td>
|
||||
<td>
|
||||
<a href=format!("/user/{}", user.id)>"Details"</a>
|
||||
<input type="checkbox"/>
|
||||
<ActionForm action=delete_user_action>
|
||||
<input type="hidden" name="id" value=user.id/>
|
||||
<input type="submit" value="Delete"/>
|
||||
</ActionForm>
|
||||
</td>
|
||||
</tr>
|
||||
</For>
|
||||
</tbody>
|
||||
</table>
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
view! {
|
||||
<section class="page">
|
||||
<form method="GET" class="search">
|
||||
<input type="search" name="q" value=q autofocus oninput="this.form.requestSubmit()"/>
|
||||
<input type="submit"/>
|
||||
</form>
|
||||
<Suspense fallback=|| view! { <p>"Loading..."</p> }>{view}</Suspense>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Details() -> impl IntoView {
|
||||
#[server]
|
||||
pub async fn get_user(id: u32) -> Result<Option<User>, ServerFnError> {
|
||||
let users = tokio::fs::read_to_string("./mock_data.json").await?;
|
||||
let data: Vec<User> = serde_json::from_str(&users)?;
|
||||
Ok(data.iter().find(|user| user.id == id).cloned())
|
||||
}
|
||||
let params = use_params_map();
|
||||
let id = move || {
|
||||
params
|
||||
.read()
|
||||
.get("id")
|
||||
.and_then(|id| id.parse::<u32>().ok())
|
||||
};
|
||||
let user = Resource::new(id, |id| async move {
|
||||
match id {
|
||||
None => Ok(None),
|
||||
Some(id) => get_user(id).await,
|
||||
}
|
||||
});
|
||||
|
||||
move || {
|
||||
Suspend::new(async move {
|
||||
user.await.map(|user| match user {
|
||||
None => Either::Left(view! {
|
||||
<section class="page">
|
||||
<h2>"Not found."</h2>
|
||||
<p>"Sorry — we couldn’t find that user."</p>
|
||||
</section>
|
||||
}),
|
||||
Some(user) => Either::Right(view! {
|
||||
<section class="page">
|
||||
<h2>{user.first_name} " " { user.last_name}</h2>
|
||||
<p class="email">{user.email}</p>
|
||||
</section>
|
||||
}),
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn About() -> impl IntoView {
|
||||
view! {
|
||||
<section class="page">
|
||||
<h2>"About"</h2>
|
||||
<p>"This demo is intended to show off an experimental “islands router” feature, which mimics the smooth transitions and user experience of client-side routing while minimizing the amount of code that actually runs in the browser."</p>
|
||||
<p>"By default, all the content in this application is only rendered on the server. But you can add client-side interactivity via islands like this one:"</p>
|
||||
<Counter/>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
#[island]
|
||||
pub fn Counter() -> impl IntoView {
|
||||
let count = RwSignal::new(0);
|
||||
view! {
|
||||
<button class="counter" on:click=move |_| *count.write() += 1>{count}</button>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,52 @@
|
||||
.pending {
|
||||
color: purple;
|
||||
body {
|
||||
font-family: system-ui, sans-serif;
|
||||
background-color: #f6f6fa;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: ui-rounded, 'Hiragino Maru Gothic ProN', Quicksand, Comfortaa, Manjari, 'Arial Rounded MT', 'Arial Rounded MT Bold', Calibri, source-sans-pro, sans-serif;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
nav {
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
nav a {
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
form.search {
|
||||
display: flex;
|
||||
margin: 2rem auto;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
td {
|
||||
min-width: 10rem;
|
||||
width: 10rem;
|
||||
}
|
||||
|
||||
table {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.page {
|
||||
width: 80%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
td:last-child > * {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.note, .note {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button.counter {
|
||||
display: block;
|
||||
font-size: 2rem;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
@@ -149,12 +149,12 @@ pub fn App() -> impl IntoView {
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="row">
|
||||
<Button id="run" text="Create 1,000 rows" on:click=run/>
|
||||
<Button id="runlots" text="Create 10,000 rows" on:click=run_lots/>
|
||||
<Button id="add" text="Append 1,000 rows" on:click=add/>
|
||||
<Button id="update" text="Update every 10th row" on:click=update/>
|
||||
<Button id="clear" text="Clear" on:click=clear/>
|
||||
<Button id="swaprows" text="Swap Rows" on:click=swap_rows/>
|
||||
<Button id="run" text="Create 1,000 rows" on:click=run />
|
||||
<Button id="runlots" text="Create 10,000 rows" on:click=run_lots />
|
||||
<Button id="add" text="Append 1,000 rows" on:click=add />
|
||||
<Button id="update" text="Update every 10th row" on:click=update />
|
||||
<Button id="clear" text="Clear" on:click=clear />
|
||||
<Button id="swaprows" text="Swap Rows" on:click=swap_rows />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,6 @@ use leptos_router::{
|
||||
},
|
||||
hooks::{use_navigate, use_params, use_query_map},
|
||||
params::Params,
|
||||
MatchNestedRoutes,
|
||||
};
|
||||
use leptos_router_macro::path;
|
||||
use std::time::Duration;
|
||||
@@ -25,7 +24,7 @@ pub fn RouterExample() -> impl IntoView {
|
||||
// contexts are passed down through the route tree
|
||||
provide_context(ExampleContext(0));
|
||||
|
||||
// this signal will be ued to set whether we are allowed to access a protected route
|
||||
// this signal will be used to set whether we are allowed to access a protected route
|
||||
let (logged_in, set_logged_in) = signal(true);
|
||||
let (is_routing, set_is_routing) = signal(false);
|
||||
|
||||
@@ -33,7 +32,7 @@ pub fn RouterExample() -> impl IntoView {
|
||||
<Router set_is_routing>
|
||||
// shows a progress bar while async data are loading
|
||||
<div class="routing-progress">
|
||||
<RoutingProgress is_routing max_time=Duration::from_millis(250)/>
|
||||
<RoutingProgress is_routing max_time=Duration::from_millis(250) />
|
||||
</div>
|
||||
<nav>
|
||||
// ordinary <a> elements can be used for client-side navigation
|
||||
@@ -53,15 +52,15 @@ pub fn RouterExample() -> impl IntoView {
|
||||
<Routes transition=true fallback=|| "This page could not be found.">
|
||||
// paths can be created using the path!() macro, or provided as types like
|
||||
// StaticSegment("about")
|
||||
<Route path=path!("about") view=About/>
|
||||
<Route path=path!("about") view=About />
|
||||
<ProtectedRoute
|
||||
path=path!("settings")
|
||||
condition=move || Some(logged_in.get())
|
||||
redirect_path=|| "/"
|
||||
view=Settings
|
||||
/>
|
||||
<Route path=path!("redirect-home") view=|| view! { <Redirect path="/"/> }/>
|
||||
<ContactRoutes/>
|
||||
<Route path=path!("redirect-home") view=|| view! { <Redirect path="/" /> } />
|
||||
<ContactRoutes />
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
@@ -71,11 +70,11 @@ pub fn RouterExample() -> impl IntoView {
|
||||
// You can define other routes in their own component.
|
||||
// Routes implement the MatchNestedRoutes
|
||||
#[component(transparent)]
|
||||
pub fn ContactRoutes() -> impl MatchNestedRoutes + Clone {
|
||||
pub fn ContactRoutes() -> impl leptos_router::MatchNestedRoutes + Clone {
|
||||
view! {
|
||||
<ParentRoute path=path!("") view=ContactList>
|
||||
<Route path=path!("/") view=|| "Select a contact."/>
|
||||
<Route path=path!("/:id") view=Contact/>
|
||||
<Route path=path!("/") view=|| "Select a contact." />
|
||||
<Route path=path!("/:id") view=Contact />
|
||||
</ParentRoute>
|
||||
}
|
||||
.into_inner()
|
||||
@@ -122,7 +121,7 @@ pub fn ContactList() -> impl IntoView {
|
||||
<Suspense fallback=move || view! { <p>"Loading contacts..."</p> }>
|
||||
<ul>{contacts}</ul>
|
||||
</Suspense>
|
||||
<Outlet/>
|
||||
<Outlet />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -166,7 +165,7 @@ pub fn Contact() -> impl IntoView {
|
||||
Some(contact) => Either::Right(view! {
|
||||
<section class="card">
|
||||
<h1>{contact.first_name} " " {contact.last_name}</h1>
|
||||
<p>{contact.address_1} <br/> {contact.address_2}</p>
|
||||
<p>{contact.address_1} <br /> {contact.address_2}</p>
|
||||
</section>
|
||||
}),
|
||||
}
|
||||
@@ -224,10 +223,10 @@ pub fn Settings() -> impl IntoView {
|
||||
<Form action="">
|
||||
<fieldset>
|
||||
<legend>"Name"</legend>
|
||||
<input type="text" name="first_name" placeholder="First"/>
|
||||
<input type="text" name="last_name" placeholder="Last"/>
|
||||
<input type="text" name="first_name" placeholder="First" />
|
||||
<input type="text" name="last_name" placeholder="Last" />
|
||||
</fieldset>
|
||||
<input type="submit"/>
|
||||
<input type="submit" />
|
||||
<p>
|
||||
"This uses the " <code>"<Form/>"</code>
|
||||
" component, which enhances forms by using client-side navigation for "
|
||||
|
||||
@@ -21,21 +21,21 @@ server_fn = { path = "../../server_fn", features = [
|
||||
log = "0.4.22"
|
||||
simple_logger = "5.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = [
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
tower = { version = "0.5.2", optional = true }
|
||||
tower-http = { version = "0.6.2", features = [
|
||||
"fs",
|
||||
"tracing",
|
||||
"trace",
|
||||
], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
thiserror = "1.0"
|
||||
thiserror = "2.0.11"
|
||||
wasm-bindgen = "0.2.93"
|
||||
serde_toml = "0.0.1"
|
||||
toml = "0.8.19"
|
||||
web-sys = { version = "0.3.70", features = ["FileList", "File"] }
|
||||
strum = { version = "0.26.3", features = ["strum_macros", "derive"] }
|
||||
notify = { version = "6.1", optional = true }
|
||||
strum = { version = "0.27.1", features = ["strum_macros", "derive"] }
|
||||
notify = { version = "8.0", optional = true }
|
||||
pin-project-lite = "0.2.14"
|
||||
dashmap = { version = "6.0", optional = true }
|
||||
once_cell = { version = "1.19", optional = true }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use futures::StreamExt;
|
||||
use futures::{Sink, Stream, StreamExt};
|
||||
use http::Method;
|
||||
use leptos::{html::Input, prelude::*, task::spawn_local};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
@@ -9,8 +9,10 @@ use server_fn::{
|
||||
MultipartFormData, Postcard, Rkyv, SerdeLite, StreamingText,
|
||||
TextStream,
|
||||
},
|
||||
error::{FromServerFnError, IntoAppError, ServerFnErrorErr},
|
||||
request::{browser::BrowserRequest, ClientReq, Req},
|
||||
response::{browser::BrowserResponse, ClientRes, Res},
|
||||
response::{browser::BrowserResponse, ClientRes, TryRes},
|
||||
ContentType,
|
||||
};
|
||||
use std::future::Future;
|
||||
#[cfg(feature = "ssr")]
|
||||
@@ -652,32 +654,72 @@ pub fn FileWatcher() -> impl IntoView {
|
||||
/// implementations if you'd like. However, it's much lighter weight to use something like `strum`
|
||||
/// simply to generate those trait implementations.
|
||||
#[server]
|
||||
pub async fn ascii_uppercase(
|
||||
text: String,
|
||||
) -> Result<String, ServerFnError<InvalidArgument>> {
|
||||
pub async fn ascii_uppercase(text: String) -> Result<String, MyErrors> {
|
||||
other_error()?;
|
||||
Ok(ascii_uppercase_inner(text)?)
|
||||
}
|
||||
|
||||
pub fn other_error() -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn ascii_uppercase_inner(text: String) -> Result<String, InvalidArgument> {
|
||||
if text.len() < 5 {
|
||||
Err(InvalidArgument::TooShort.into())
|
||||
Err(InvalidArgument::TooShort)
|
||||
} else if text.len() > 15 {
|
||||
Err(InvalidArgument::TooLong.into())
|
||||
Err(InvalidArgument::TooLong)
|
||||
} else if text.is_ascii() {
|
||||
Ok(text.to_ascii_uppercase())
|
||||
} else {
|
||||
Err(InvalidArgument::NotAscii.into())
|
||||
Err(InvalidArgument::NotAscii)
|
||||
}
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn ascii_uppercase_classic(
|
||||
text: String,
|
||||
) -> Result<String, ServerFnError<InvalidArgument>> {
|
||||
Ok(ascii_uppercase_inner(text)?)
|
||||
}
|
||||
|
||||
// The EnumString and Display derive macros are provided by strum
|
||||
#[derive(Debug, Clone, EnumString, Display)]
|
||||
#[derive(Debug, Clone, Display, EnumString, Serialize, Deserialize)]
|
||||
pub enum InvalidArgument {
|
||||
TooShort,
|
||||
TooLong,
|
||||
NotAscii,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Display, Serialize, Deserialize)]
|
||||
pub enum MyErrors {
|
||||
InvalidArgument(InvalidArgument),
|
||||
ServerFnError(ServerFnErrorErr),
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl From<InvalidArgument> for MyErrors {
|
||||
fn from(value: InvalidArgument) -> Self {
|
||||
MyErrors::InvalidArgument(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for MyErrors {
|
||||
fn from(value: String) -> Self {
|
||||
MyErrors::Other(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromServerFnError for MyErrors {
|
||||
fn from_server_fn_error(value: ServerFnErrorErr) -> Self {
|
||||
MyErrors::ServerFnError(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn CustomErrorTypes() -> impl IntoView {
|
||||
let input_ref = NodeRef::<Input>::new();
|
||||
let (result, set_result) = signal(None);
|
||||
let (result_classic, set_result_classic) = signal(None);
|
||||
|
||||
view! {
|
||||
<h3>Using custom error types</h3>
|
||||
@@ -692,14 +734,17 @@ pub fn CustomErrorTypes() -> impl IntoView {
|
||||
<button on:click=move |_| {
|
||||
let value = input_ref.get().unwrap().value();
|
||||
spawn_local(async move {
|
||||
let data = ascii_uppercase(value).await;
|
||||
let data = ascii_uppercase(value.clone()).await;
|
||||
let data_classic = ascii_uppercase_classic(value).await;
|
||||
set_result.set(Some(data));
|
||||
set_result_classic.set(Some(data_classic));
|
||||
});
|
||||
}>
|
||||
|
||||
"Submit"
|
||||
</button>
|
||||
<p>{move || format!("{:?}", result.get())}</p>
|
||||
<p>{move || format!("{:?}", result_classic.get())}</p>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -717,8 +762,11 @@ pub struct Toml;
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct TomlEncoded<T>(T);
|
||||
|
||||
impl Encoding for Toml {
|
||||
impl ContentType for Toml {
|
||||
const CONTENT_TYPE: &'static str = "application/toml";
|
||||
}
|
||||
|
||||
impl Encoding for Toml {
|
||||
const METHOD: Method = Method::POST;
|
||||
}
|
||||
|
||||
@@ -726,14 +774,12 @@ impl<T, Request, Err> IntoReq<Toml, Request, Err> for TomlEncoded<T>
|
||||
where
|
||||
Request: ClientReq<Err>,
|
||||
T: Serialize,
|
||||
Err: FromServerFnError,
|
||||
{
|
||||
fn into_req(
|
||||
self,
|
||||
path: &str,
|
||||
accepts: &str,
|
||||
) -> Result<Request, ServerFnError<Err>> {
|
||||
let data = toml::to_string(&self.0)
|
||||
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
|
||||
fn into_req(self, path: &str, accepts: &str) -> Result<Request, Err> {
|
||||
let data = toml::to_string(&self.0).map_err(|e| {
|
||||
ServerFnErrorErr::Serialization(e.to_string()).into_app_error()
|
||||
})?;
|
||||
Request::try_new_post(path, Toml::CONTENT_TYPE, accepts, data)
|
||||
}
|
||||
}
|
||||
@@ -742,23 +788,26 @@ impl<T, Request, Err> FromReq<Toml, Request, Err> for TomlEncoded<T>
|
||||
where
|
||||
Request: Req<Err> + Send,
|
||||
T: DeserializeOwned,
|
||||
Err: FromServerFnError,
|
||||
{
|
||||
async fn from_req(req: Request) -> Result<Self, ServerFnError<Err>> {
|
||||
async fn from_req(req: Request) -> Result<Self, Err> {
|
||||
let string_data = req.try_into_string().await?;
|
||||
toml::from_str::<T>(&string_data)
|
||||
.map(TomlEncoded)
|
||||
.map_err(|e| ServerFnError::Args(e.to_string()))
|
||||
.map_err(|e| ServerFnErrorErr::Args(e.to_string()).into_app_error())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Response, Err> IntoRes<Toml, Response, Err> for TomlEncoded<T>
|
||||
where
|
||||
Response: Res<Err>,
|
||||
Response: TryRes<Err>,
|
||||
T: Serialize + Send,
|
||||
Err: FromServerFnError,
|
||||
{
|
||||
async fn into_res(self) -> Result<Response, ServerFnError<Err>> {
|
||||
let data = toml::to_string(&self.0)
|
||||
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
|
||||
async fn into_res(self) -> Result<Response, Err> {
|
||||
let data = toml::to_string(&self.0).map_err(|e| {
|
||||
ServerFnErrorErr::Serialization(e.to_string()).into_app_error()
|
||||
})?;
|
||||
Response::try_from_string(Toml::CONTENT_TYPE, data)
|
||||
}
|
||||
}
|
||||
@@ -767,12 +816,13 @@ impl<T, Response, Err> FromRes<Toml, Response, Err> for TomlEncoded<T>
|
||||
where
|
||||
Response: ClientRes<Err> + Send,
|
||||
T: DeserializeOwned,
|
||||
Err: FromServerFnError,
|
||||
{
|
||||
async fn from_res(res: Response) -> Result<Self, ServerFnError<Err>> {
|
||||
async fn from_res(res: Response) -> Result<Self, Err> {
|
||||
let data = res.try_into_string().await?;
|
||||
toml::from_str(&data)
|
||||
.map(TomlEncoded)
|
||||
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
|
||||
toml::from_str(&data).map(TomlEncoded).map_err(|e| {
|
||||
ServerFnErrorErr::Deserialization(e.to_string()).into_app_error()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -835,7 +885,10 @@ pub fn CustomClientExample() -> impl IntoView {
|
||||
pub struct CustomClient;
|
||||
|
||||
// Implement the `Client` trait for it.
|
||||
impl<CustErr> Client<CustErr> for CustomClient {
|
||||
impl<E> Client<E> for CustomClient
|
||||
where
|
||||
E: FromServerFnError,
|
||||
{
|
||||
// BrowserRequest and BrowserResponse are the defaults used by other server functions.
|
||||
// They are wrappers for the underlying Web Fetch API types.
|
||||
type Request = BrowserRequest;
|
||||
@@ -844,8 +897,7 @@ pub fn CustomClientExample() -> impl IntoView {
|
||||
// Our custom `send()` implementation does all the work.
|
||||
fn send(
|
||||
req: Self::Request,
|
||||
) -> impl Future<Output = Result<Self::Response, ServerFnError<CustErr>>>
|
||||
+ Send {
|
||||
) -> impl Future<Output = Result<Self::Response, E>> + Send {
|
||||
// BrowserRequest derefs to the underlying Request type from gloo-net,
|
||||
// so we can get access to the headers here
|
||||
let headers = req.headers();
|
||||
@@ -854,6 +906,24 @@ pub fn CustomClientExample() -> impl IntoView {
|
||||
// delegate back out to BrowserClient to send the modified request
|
||||
BrowserClient::send(req)
|
||||
}
|
||||
|
||||
fn open_websocket(
|
||||
path: &str,
|
||||
) -> impl Future<
|
||||
Output = Result<
|
||||
(
|
||||
impl Stream<Item = Result<server_fn::Bytes, E>> + Send + 'static,
|
||||
impl Sink<Result<server_fn::Bytes, E>> + Send + 'static,
|
||||
),
|
||||
E,
|
||||
>,
|
||||
> + Send {
|
||||
BrowserClient::open_websocket(path)
|
||||
}
|
||||
|
||||
fn spawn(future: impl Future<Output = ()> + Send + 'static) {
|
||||
<BrowserClient as Client<E>>::spawn(future)
|
||||
}
|
||||
}
|
||||
|
||||
// Specify our custom client with `client = `
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly-2024-01-29"
|
||||
channel = "nightly-2025-03-05"
|
||||
|
||||
@@ -20,7 +20,7 @@ leptos_router = { path = "../../router" }
|
||||
log = "0.4.22"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
thiserror = "1.0"
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = [
|
||||
|
||||
@@ -18,7 +18,7 @@ leptos_router = { path = "../../router" }
|
||||
log = "0.4.22"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
thiserror = "1.0"
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = [
|
||||
@@ -45,7 +45,7 @@ ssr = [
|
||||
"dep:leptos_axum",
|
||||
"leptos_router/ssr",
|
||||
"dep:notify",
|
||||
"dep:http"
|
||||
"dep:http",
|
||||
]
|
||||
|
||||
[profile.release]
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
@check_instrumented_issue_3719
|
||||
Feature: Using instrumented counters to test regression from #3502.
|
||||
Check that the suspend/suspense and the underlying resources are
|
||||
called with the expected number of times. If this was already in
|
||||
place by #3502 (5c43c18) it should have caught this regression.
|
||||
For a better minimum demonstration see #3719.
|
||||
|
||||
Background:
|
||||
|
||||
Given I see the app
|
||||
And I select the mode Instrumented
|
||||
|
||||
Scenario: follow all paths via CSR avoids #3502
|
||||
Given I select the following links
|
||||
| Item Listing |
|
||||
| Item 1 |
|
||||
| Inspect path2 |
|
||||
| Inspect path2/field3 |
|
||||
And I click on Reset CSR Counters
|
||||
When I select the following links
|
||||
| Inspect path2/field1 |
|
||||
| Inspect path2/field2 |
|
||||
And I go check the Counters
|
||||
Then I see the following counters under section
|
||||
| Suspend Calls | |
|
||||
| item_listing | 0 |
|
||||
| item_overview | 0 |
|
||||
| item_inspect | 2 |
|
||||
And the following counters under section
|
||||
| Server Calls (CSR) | |
|
||||
| list_items | 0 |
|
||||
| get_item | 0 |
|
||||
| inspect_item_root | 0 |
|
||||
| inspect_item_field | 2 |
|
||||
|
||||
# To show that starting directly from within a param will simply
|
||||
# cause the problem.
|
||||
Scenario: Quicker way to demonstrate regression caused by #3502
|
||||
Given I select the link Target 123
|
||||
# And I click on Reset CSR Counters
|
||||
When I select the following links
|
||||
| Inspect path2/field1 |
|
||||
| Inspect path2/field2 |
|
||||
And I go check the Counters
|
||||
Then I see the following counters under section
|
||||
| Suspend Calls | |
|
||||
| item_listing | 0 |
|
||||
| item_overview | 0 |
|
||||
| item_inspect | 3 |
|
||||
And the following counters under section
|
||||
| Server Calls (CSR) | |
|
||||
| list_items | 1 |
|
||||
| get_item | 1 |
|
||||
| inspect_item_root | 0 |
|
||||
| inspect_item_field | 4 |
|
||||
|
||||
Scenario: Follow paths ordinarily down to a target
|
||||
Given I select the following links
|
||||
| Item Listing |
|
||||
| Item 1 |
|
||||
And I click on Reset CSR Counters
|
||||
When I select the following links
|
||||
| Target 4## |
|
||||
| Target 3## |
|
||||
And I go check the Counters
|
||||
Then I see the following counters under section
|
||||
| Suspend Calls | |
|
||||
| item_listing | 0 |
|
||||
| item_overview | 2 |
|
||||
| item_inspect | 0 |
|
||||
And the following counters under section
|
||||
| Server Calls (CSR) | |
|
||||
| list_items | 0 |
|
||||
| get_item | 2 |
|
||||
| inspect_item_root | 0 |
|
||||
| inspect_item_field | 0 |
|
||||
|
||||
Scenario: Same as above, but add a refresh to test hydration
|
||||
Given I select the following links
|
||||
| Item Listing |
|
||||
| Item 1 |
|
||||
And I refresh the page
|
||||
And I click on Reset CSR Counters
|
||||
When I select the following links
|
||||
| Target 4## |
|
||||
| Target 3## |
|
||||
And I go check the Counters
|
||||
Then I see the following counters under section
|
||||
| Suspend Calls | |
|
||||
| item_listing | 0 |
|
||||
| item_overview | 2 |
|
||||
| item_inspect | 0 |
|
||||
And the following counters under section
|
||||
| Server Calls (CSR) | |
|
||||
| list_items | 0 |
|
||||
| get_item | 2 |
|
||||
| inspect_item_root | 0 |
|
||||
| inspect_item_field | 0 |
|
||||
|
||||
@@ -3,12 +3,28 @@ mod fixtures;
|
||||
use anyhow::Result;
|
||||
use cucumber::World;
|
||||
use fixtures::world::AppWorld;
|
||||
use std::{ffi::OsStr, fs::read_dir};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
AppWorld::cucumber()
|
||||
.fail_on_skipped()
|
||||
.run_and_exit("./features")
|
||||
.await;
|
||||
// Normally the below is done, but it's now gotten to the point of
|
||||
// having a sufficient number of tests where the resource contention
|
||||
// of the concurrently running browsers will cause failures on CI.
|
||||
// AppWorld::cucumber()
|
||||
// .fail_on_skipped()
|
||||
// .run_and_exit("./features")
|
||||
// .await;
|
||||
|
||||
// Mitigate the issue by manually stepping through each feature,
|
||||
// rather than letting cucumber glob them and dispatch all at once.
|
||||
for entry in read_dir("./features")? {
|
||||
let path = entry?.path();
|
||||
if path.extension() == Some(OsStr::new("feature")) {
|
||||
AppWorld::cucumber()
|
||||
.fail_on_skipped()
|
||||
.run_and_exit(path)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use leptos_router::{
|
||||
hooks::use_params,
|
||||
nested_router::Outlet,
|
||||
params::Params,
|
||||
MatchNestedRoutes, ParamSegment, SsrMode, StaticSegment, WildcardSegment,
|
||||
ParamSegment, SsrMode, StaticSegment, WildcardSegment,
|
||||
};
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
@@ -21,6 +21,7 @@ pub(super) mod counter {
|
||||
pub struct Counter(AtomicU32);
|
||||
|
||||
impl Counter {
|
||||
#[allow(dead_code)]
|
||||
pub const fn new() -> Self {
|
||||
Self(AtomicU32::new(0))
|
||||
}
|
||||
@@ -203,20 +204,20 @@ pub struct SuspenseCounters {
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn InstrumentedRoutes() -> impl MatchNestedRoutes + Clone {
|
||||
pub fn InstrumentedRoutes() -> impl leptos_router::MatchNestedRoutes + Clone {
|
||||
// TODO should make this mode configurable via feature flag?
|
||||
let ssr = SsrMode::Async;
|
||||
view! {
|
||||
<ParentRoute path=StaticSegment("instrumented") view=InstrumentedRoot ssr>
|
||||
<Route path=StaticSegment("/") view=InstrumentedTop/>
|
||||
<Route path=StaticSegment("/") view=InstrumentedTop />
|
||||
<ParentRoute path=StaticSegment("item") view=ItemRoot>
|
||||
<Route path=StaticSegment("/") view=ItemListing/>
|
||||
<Route path=StaticSegment("/") view=ItemListing />
|
||||
<ParentRoute path=ParamSegment("id") view=ItemTop>
|
||||
<Route path=StaticSegment("/") view=ItemOverview/>
|
||||
<Route path=WildcardSegment("path") view=ItemInspect/>
|
||||
<Route path=StaticSegment("/") view=ItemOverview />
|
||||
<Route path=WildcardSegment("path") view=ItemInspect />
|
||||
</ParentRoute>
|
||||
</ParentRoute>
|
||||
<Route path=StaticSegment("counters") view=ShowCounters/>
|
||||
<Route path=StaticSegment("counters") view=ShowCounters />
|
||||
</ParentRoute>
|
||||
}
|
||||
.into_inner()
|
||||
@@ -279,32 +280,41 @@ fn InstrumentedRoot() -> impl IntoView {
|
||||
<section id="instrumented">
|
||||
<nav>
|
||||
<a href="/">"Site Root"</a>
|
||||
<A href="./" exact=true>"Instrumented Root"</A>
|
||||
<A href="item/" strict_trailing_slash=true>"Item Listing"</A>
|
||||
<A href="counters" strict_trailing_slash=true>"Counters"</A>
|
||||
<A href="./" exact=true>
|
||||
"Instrumented Root"
|
||||
</A>
|
||||
<A href="item/" strict_trailing_slash=true>
|
||||
"Item Listing"
|
||||
</A>
|
||||
<A href="counters" strict_trailing_slash=true>
|
||||
"Counters"
|
||||
</A>
|
||||
</nav>
|
||||
<FieldNavPortlet/>
|
||||
<Outlet/>
|
||||
<Suspense>{
|
||||
move || Suspend::new(async move {
|
||||
<FieldNavPortlet />
|
||||
<Outlet />
|
||||
<Suspense>
|
||||
{move || Suspend::new(async move {
|
||||
let clear_suspense_counters = move |_| {
|
||||
counters.update(|c| *c = SuspenseCounters::default());
|
||||
};
|
||||
csr_ticket.get().map(|ticket| {
|
||||
let ticket = ticket.0;
|
||||
view! {
|
||||
<ActionForm action=reset_counters>
|
||||
<input type="hidden" name="ticket" value=format!("{ticket}") />
|
||||
<input
|
||||
id="reset-csr-counters"
|
||||
type="submit"
|
||||
value="Reset CSR Counters"
|
||||
on:click=clear_suspense_counters/>
|
||||
</ActionForm>
|
||||
}
|
||||
})
|
||||
})
|
||||
}</Suspense>
|
||||
csr_ticket
|
||||
.get()
|
||||
.map(|ticket| {
|
||||
let ticket = ticket.0;
|
||||
view! {
|
||||
<ActionForm action=reset_counters>
|
||||
<input type="hidden" name="ticket" value=format!("{ticket}") />
|
||||
<input
|
||||
id="reset-csr-counters"
|
||||
type="submit"
|
||||
value="Reset CSR Counters"
|
||||
on:click=clear_suspense_counters
|
||||
/>
|
||||
</ActionForm>
|
||||
}
|
||||
})
|
||||
})}
|
||||
</Suspense>
|
||||
<footer>
|
||||
<nav>
|
||||
<A href="item/3/">"Target 3##"</A>
|
||||
@@ -323,11 +333,17 @@ fn InstrumentedRoot() -> impl IntoView {
|
||||
fn InstrumentedTop() -> impl IntoView {
|
||||
view! {
|
||||
<h1>"Instrumented Tests"</h1>
|
||||
<p>"These tests validates the number of invocations of server functions and suspenses per access."</p>
|
||||
<p>
|
||||
"These tests validates the number of invocations of server functions and suspenses per access."
|
||||
</p>
|
||||
<ul>
|
||||
// not using `A` because currently some bugs with artix
|
||||
<li><a href="item/">"Item Listing"</a></li>
|
||||
<li><a href="item/4/path1/">"Target 41#"</a></li>
|
||||
<li>
|
||||
<a href="item/">"Item Listing"</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="item/4/path1/">"Target 41#"</a>
|
||||
</li>
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
@@ -342,7 +358,7 @@ fn ItemRoot() -> impl IntoView {
|
||||
|
||||
view! {
|
||||
<h2>"<ItemRoot/>"</h2>
|
||||
<Outlet/>
|
||||
<Outlet />
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,7 +376,9 @@ fn ItemListing() -> impl IntoView {
|
||||
// adding an extra `/` in artix; manually construct `a` instead.
|
||||
// <li><A href=format!("./{item}/")>"Item "{item}</A></li>
|
||||
view! {
|
||||
<li><a href=format!("/instrumented/item/{item}/")>"Item "{item}</a></li>
|
||||
<li>
|
||||
<a href=format!("/instrumented/item/{item}/")>"Item "{item}</a>
|
||||
</li>
|
||||
}
|
||||
)
|
||||
.collect_view()
|
||||
@@ -373,9 +391,7 @@ fn ItemListing() -> impl IntoView {
|
||||
view! {
|
||||
<h3>"<ItemListing/>"</h3>
|
||||
<ul>
|
||||
<Suspense>
|
||||
{item_listing}
|
||||
</Suspense>
|
||||
<Suspense>{item_listing}</Suspense>
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
@@ -402,7 +418,7 @@ fn ItemTop() -> impl IntoView {
|
||||
));
|
||||
view! {
|
||||
<h4>"<ItemTop/>"</h4>
|
||||
<Outlet/>
|
||||
<Outlet />
|
||||
}
|
||||
}
|
||||
|
||||
@@ -412,24 +428,29 @@ fn ItemOverview() -> impl IntoView {
|
||||
let resource = expect_context::<Resource<Option<GetItemResult>>>();
|
||||
let item_view = move || {
|
||||
Suspend::new(async move {
|
||||
let result = resource.await.map(|GetItemResult(item, names)| view! {
|
||||
<p>{format!("Viewing {item:?}")}</p>
|
||||
<ul>{
|
||||
names.into_iter()
|
||||
.map(|name| {
|
||||
// FIXME seems like relative link isn't working, it is currently
|
||||
// adding an extra `/` in artix; manually construct `a` instead.
|
||||
// <li><A href=format!("./{name}/")>{format!("Inspect {name}")}</A></li>
|
||||
let id = item.id;
|
||||
view! {
|
||||
<li><a href=format!("/instrumented/item/{id}/{name}/")>
|
||||
"Inspect "{name.clone()}
|
||||
</a></li>
|
||||
}
|
||||
})
|
||||
.collect_view()
|
||||
}</ul>
|
||||
});
|
||||
let result = resource.await.map(|GetItemResult(item, names)| {
|
||||
view! {
|
||||
<p>{format!("Viewing {item:?}")}</p>
|
||||
<ul>
|
||||
{names
|
||||
.into_iter()
|
||||
.map(|name| {
|
||||
let id = item.id;
|
||||
// FIXME seems like relative link isn't working, it is currently
|
||||
// adding an extra `/` in artix; manually construct `a` instead.
|
||||
// <li><A href=format!("./{name}/")>{format!("Inspect {name}")}</A></li>
|
||||
view! {
|
||||
<li>
|
||||
<a href=format!(
|
||||
"/instrumented/item/{id}/{name}/",
|
||||
)>"Inspect "{name.clone()}</a>
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect_view()}
|
||||
</ul>
|
||||
}
|
||||
});
|
||||
suspense_counters.update_untracked(|c| c.item_overview += 1);
|
||||
result
|
||||
})
|
||||
@@ -437,9 +458,7 @@ fn ItemOverview() -> impl IntoView {
|
||||
|
||||
view! {
|
||||
<h5>"<ItemOverview/>"</h5>
|
||||
<Suspense>
|
||||
{item_view}
|
||||
</Suspense>
|
||||
<Suspense>{item_view}</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -473,8 +492,9 @@ fn ItemInspect() -> impl IntoView {
|
||||
// result
|
||||
},
|
||||
);
|
||||
on_cleanup(|| {
|
||||
if let Some(c) = use_context::<WriteSignal<Option<FieldNavCtx>>>() {
|
||||
let ws = use_context::<WriteSignal<Option<FieldNavCtx>>>();
|
||||
on_cleanup(move || {
|
||||
if let Some(c) = ws {
|
||||
c.set(None);
|
||||
}
|
||||
});
|
||||
@@ -496,23 +516,26 @@ fn ItemInspect() -> impl IntoView {
|
||||
));
|
||||
view! {
|
||||
<p>{format!("Inspecting {item:?}")}</p>
|
||||
<ul>{
|
||||
fields.iter()
|
||||
<ul>
|
||||
{fields
|
||||
.iter()
|
||||
.map(|field| {
|
||||
// FIXME seems like relative link to root for a wildcard isn't
|
||||
// working as expected, so manually construct `a` instead.
|
||||
// let text = format!("Inspect {name}/{field}");
|
||||
// view! {
|
||||
// <li><A href=format!("{field}")>{text}</A></li>
|
||||
// <li><A href=format!("{field}")>{text}</A></li>
|
||||
// }
|
||||
view! {
|
||||
<li><a href=format!("/instrumented/item/{id}/{name}/{field}")>{
|
||||
format!("Inspect {name}/{field}")
|
||||
}</a></li>
|
||||
<li>
|
||||
<a href=format!(
|
||||
"/instrumented/item/{id}/{name}/{field}",
|
||||
)>{format!("Inspect {name}/{field}")}</a>
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect_view()
|
||||
}</ul>
|
||||
.collect_view()}
|
||||
</ul>
|
||||
}
|
||||
});
|
||||
suspense_counters.update_untracked(|c| c.item_inspect += 1);
|
||||
@@ -527,9 +550,7 @@ fn ItemInspect() -> impl IntoView {
|
||||
|
||||
view! {
|
||||
<h5>"<ItemInspect/>"</h5>
|
||||
<Suspense>
|
||||
{inspect_view}
|
||||
</Suspense>
|
||||
<Suspense>{inspect_view}</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -590,7 +611,8 @@ fn ShowCounters() -> impl IntoView {
|
||||
id="reset-counters"
|
||||
type="submit"
|
||||
value="Reset Counters"
|
||||
on:click=clear_suspense_counters/>
|
||||
on:click=clear_suspense_counters
|
||||
/>
|
||||
</ActionForm>
|
||||
}
|
||||
})
|
||||
@@ -601,20 +623,23 @@ fn ShowCounters() -> impl IntoView {
|
||||
<h2>"Counters"</h2>
|
||||
|
||||
<h3 id="suspend-calls">"Suspend Calls"</h3>
|
||||
{move || suspense_counters.with(|c| view! {
|
||||
<dl>
|
||||
<dt>"item_listing"</dt>
|
||||
<dd id="item_listing">{c.item_listing}</dd>
|
||||
<dt>"item_overview"</dt>
|
||||
<dd id="item_overview">{c.item_overview}</dd>
|
||||
<dt>"item_inspect"</dt>
|
||||
<dd id="item_inspect">{c.item_inspect}</dd>
|
||||
</dl>
|
||||
})}
|
||||
{move || {
|
||||
suspense_counters
|
||||
.with(|c| {
|
||||
view! {
|
||||
<dl>
|
||||
<dt>"item_listing"</dt>
|
||||
<dd id="item_listing">{c.item_listing}</dd>
|
||||
<dt>"item_overview"</dt>
|
||||
<dd id="item_overview">{c.item_overview}</dd>
|
||||
<dt>"item_inspect"</dt>
|
||||
<dd id="item_inspect">{c.item_inspect}</dd>
|
||||
</dl>
|
||||
}
|
||||
})
|
||||
}}
|
||||
|
||||
<Suspense>
|
||||
{counter_view}
|
||||
</Suspense>
|
||||
<Suspense>{counter_view}</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -642,17 +667,17 @@ pub fn FieldNavPortlet() -> impl IntoView {
|
||||
view! {
|
||||
<div id="FieldNavPortlet">
|
||||
<span>"FieldNavPortlet:"</span>
|
||||
<nav>{
|
||||
ctx.0.map(|ctx| {
|
||||
ctx.into_iter()
|
||||
.map(|FieldNavItem { href, text }| {
|
||||
view! {
|
||||
<A href=href>{text}</A>
|
||||
}
|
||||
})
|
||||
.collect_view()
|
||||
})
|
||||
}</nav>
|
||||
<nav>
|
||||
{ctx
|
||||
.0
|
||||
.map(|ctx| {
|
||||
ctx.into_iter()
|
||||
.map(|FieldNavItem { href, text }| {
|
||||
view! { <A href=href>{text}</A> }
|
||||
})
|
||||
.collect_view()
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
}
|
||||
})
|
||||
|
||||
@@ -7,7 +7,7 @@ edition = "2021"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
console_error_panic_hook = "0.1.7"
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos_meta = { path = "../../meta" }
|
||||
|
||||
22
examples/tailwind_axum/package-lock.json
generated
Normal file
22
examples/tailwind_axum/package-lock.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "leptos-tailwind",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "leptos-tailwind",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"tailwindcss": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.0.tgz",
|
||||
"integrity": "sha512-ULRPI3A+e39T7pSaf1xoi58AqqJxVCLg8F/uM5A3FadUbnyDTgltVnXJvdkTjwCOGA6NazqHVcwPJC5h2vRYVQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
||||
69
examples/tailwind_axum/src/style.css
Normal file
69
examples/tailwind_axum/src/style.css
Normal file
@@ -0,0 +1,69 @@
|
||||
/*! tailwindcss v4.0.0 | MIT License | https://tailwindcss.com */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
.m-auto {
|
||||
margin: auto;
|
||||
}
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
.min-h-screen {
|
||||
min-height: 100vh;
|
||||
}
|
||||
.transform {
|
||||
transform: var(--tw-rotate-x) var(--tw-rotate-y) var(--tw-rotate-z) var(--tw-skew-x) var(--tw-skew-y);
|
||||
}
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
.flex-row-reverse {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.border-b-4 {
|
||||
border-bottom-style: var(--tw-border-style);
|
||||
border-bottom-width: 4px;
|
||||
}
|
||||
.border-l-2 {
|
||||
border-left-style: var(--tw-border-style);
|
||||
border-left-width: 2px;
|
||||
}
|
||||
.bg-gradient-to-tl {
|
||||
--tw-gradient-position: to top left in oklab,;
|
||||
background-image: linear-gradient(var(--tw-gradient-stops));
|
||||
}
|
||||
@property --tw-rotate-x {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: rotateX(0);
|
||||
}
|
||||
@property --tw-rotate-y {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: rotateY(0);
|
||||
}
|
||||
@property --tw-rotate-z {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: rotateZ(0);
|
||||
}
|
||||
@property --tw-skew-x {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: skewX(0);
|
||||
}
|
||||
@property --tw-skew-y {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: skewY(0);
|
||||
}
|
||||
@property --tw-border-style {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: solid;
|
||||
}
|
||||
@@ -16,7 +16,7 @@ leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
log = "0.4.22"
|
||||
simple_logger = "5.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::todo::*;
|
||||
#[cfg(feature = "ssr")]
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::Path,
|
||||
@@ -8,10 +8,9 @@ use axum::{
|
||||
Router,
|
||||
};
|
||||
use leptos::prelude::*;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use todo_app_sqlite_axum::*;
|
||||
|
||||
//Define a handler to test extractor with state
|
||||
#[cfg(feature = "ssr")]
|
||||
async fn custom_handler(
|
||||
Path(id): Path<String>,
|
||||
req: Request<Body>,
|
||||
@@ -20,14 +19,16 @@ async fn custom_handler(
|
||||
move || {
|
||||
provide_context(id.clone());
|
||||
},
|
||||
TodoApp,
|
||||
todo::TodoApp,
|
||||
);
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use crate::todo::ssr::db;
|
||||
use crate::todo::{ssr::db, *};
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
|
||||
simple_logger::init_with_level(log::Level::Error)
|
||||
.expect("couldn't initialize logging");
|
||||
@@ -45,7 +46,7 @@ async fn main() {
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/special/:id", get(custom_handler))
|
||||
.route("/special/{id}", get(custom_handler))
|
||||
.leptos_routes(&leptos_options, routes, {
|
||||
let leptos_options = leptos_options.clone();
|
||||
move || shell(leptos_options.clone())
|
||||
@@ -61,3 +62,12 @@ async fn main() {
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn main() {
|
||||
use leptos::mount::mount_to_body;
|
||||
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(todo::TodoApp);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ leptos_meta = { path = "../../meta" }
|
||||
leptos_router = { path = "../../router" }
|
||||
leptos_integration_utils = { path = "../../integrations/utils", optional = true }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
tower = { version = "0.5.1", features = ["util"], optional = true }
|
||||
tower-http = { version = "0.6.1", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
|
||||
@@ -34,7 +34,7 @@ async fn main() {
|
||||
// here, we're not actually doing server side rendering, so we set up a manual
|
||||
// handler for the server fns
|
||||
// this should include a get() handler if you have any GetUrl-based server fns
|
||||
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
|
||||
.route("/api/{*fn_name}", post(leptos_axum::handle_server_fns))
|
||||
.fallback(file_or_index_handler)
|
||||
.with_state(leptos_options);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "hydration_context"
|
||||
version = "0.2.1"
|
||||
version = "0.3.0"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -14,7 +14,7 @@ throw_error = { workspace = true }
|
||||
or_poisoned = { workspace = true }
|
||||
futures = "0.3.31"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
wasm-bindgen = { version = "0.2.97", optional = true }
|
||||
wasm-bindgen = { version = "0.2.100", optional = true }
|
||||
js-sys = { version = "0.3.74", optional = true }
|
||||
once_cell = "1.20"
|
||||
pin-project-lite = "0.2.15"
|
||||
|
||||
@@ -21,10 +21,11 @@ leptos_macro = { workspace = true, features = ["actix"] }
|
||||
leptos_meta = { workspace = true, features = ["nonce"] }
|
||||
leptos_router = { workspace = true, features = ["ssr"] }
|
||||
server_fn = { workspace = true, features = ["actix"] }
|
||||
serde_json = "1.0"
|
||||
tachys = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
parking_lot = "0.12.3"
|
||||
tracing = { version = "0.1", optional = true }
|
||||
tokio = { version = "1.41", features = ["rt", "fs"] }
|
||||
tokio = { version = "1.43", features = ["rt", "fs"] }
|
||||
send_wrapper = "0.6.0"
|
||||
dashmap = "6"
|
||||
once_cell = "1"
|
||||
@@ -33,7 +34,7 @@ once_cell = "1"
|
||||
rustdoc-args = ["--generate-link-to-definition"]
|
||||
|
||||
[features]
|
||||
dont-use-islands-router = []
|
||||
islands-router = ["tachys/islands"]
|
||||
tracing = ["dep:tracing"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
|
||||
@@ -23,6 +23,7 @@ use hydration_context::SsrSharedContext;
|
||||
use leptos::{
|
||||
config::LeptosOptions,
|
||||
context::{provide_context, use_context},
|
||||
hydration::IslandsRouterNavigation,
|
||||
prelude::expect_context,
|
||||
reactive::{computed::ScopedFuture, owner::Owner},
|
||||
IntoView,
|
||||
@@ -274,14 +275,13 @@ pub fn redirect(path: &str) {
|
||||
///
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
///
|
||||
/// ```
|
||||
/// ```no_run
|
||||
/// use actix_web::*;
|
||||
///
|
||||
/// fn register_server_functions() {
|
||||
/// // call ServerFn::register() for each of the server functions you've defined
|
||||
/// }
|
||||
///
|
||||
/// # if false { // don't actually try to run a server in a doctest...
|
||||
/// #[actix_web::main]
|
||||
/// async fn main() -> std::io::Result<()> {
|
||||
/// // make sure you actually register your server functions
|
||||
@@ -297,7 +297,6 @@ pub fn redirect(path: &str) {
|
||||
/// .run()
|
||||
/// .await
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
@@ -369,7 +368,6 @@ pub fn handle_server_fns_with_context(
|
||||
// actually run the server fn
|
||||
let mut res = ActixResponse(
|
||||
service
|
||||
.0
|
||||
.run(ActixRequest::from((req, payload)))
|
||||
.await
|
||||
.take(),
|
||||
@@ -433,7 +431,7 @@ pub fn handle_server_fns_with_context(
|
||||
/// but requires some client-side JavaScript.
|
||||
///
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
/// ```
|
||||
/// ```no_run
|
||||
/// use actix_web::{App, HttpServer};
|
||||
/// use leptos::prelude::*;
|
||||
/// use leptos_router::Method;
|
||||
@@ -444,7 +442,6 @@ pub fn handle_server_fns_with_context(
|
||||
/// view! { <main>"Hello, world!"</main> }
|
||||
/// }
|
||||
///
|
||||
/// # if false { // don't actually try to run a server in a doctest...
|
||||
/// #[actix_web::main]
|
||||
/// async fn main() -> std::io::Result<()> {
|
||||
/// let conf = get_configuration(Some("Cargo.toml")).unwrap();
|
||||
@@ -464,7 +461,6 @@ pub fn handle_server_fns_with_context(
|
||||
/// .run()
|
||||
/// .await
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
@@ -492,7 +488,7 @@ where
|
||||
/// sending down its HTML. The app will become interactive once it has fully loaded.
|
||||
///
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
/// ```
|
||||
/// ```no_run
|
||||
/// use actix_web::{App, HttpServer};
|
||||
/// use leptos::prelude::*;
|
||||
/// use leptos_router::Method;
|
||||
@@ -503,7 +499,6 @@ where
|
||||
/// view! { <main>"Hello, world!"</main> }
|
||||
/// }
|
||||
///
|
||||
/// # if false { // don't actually try to run a server in a doctest...
|
||||
/// #[actix_web::main]
|
||||
/// async fn main() -> std::io::Result<()> {
|
||||
/// let conf = get_configuration(Some("Cargo.toml")).unwrap();
|
||||
@@ -526,7 +521,6 @@ where
|
||||
/// .run()
|
||||
/// .await
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
@@ -552,7 +546,7 @@ where
|
||||
/// `async` resources have loaded.
|
||||
///
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
/// ```
|
||||
/// ```no_run
|
||||
/// use actix_web::{App, HttpServer};
|
||||
/// use leptos::prelude::*;
|
||||
/// use leptos_router::Method;
|
||||
@@ -563,7 +557,6 @@ where
|
||||
/// view! { <main>"Hello, world!"</main> }
|
||||
/// }
|
||||
///
|
||||
/// # if false { // don't actually try to run a server in a doctest...
|
||||
/// #[actix_web::main]
|
||||
/// async fn main() -> std::io::Result<()> {
|
||||
/// let conf = get_configuration(Some("Cargo.toml")).unwrap();
|
||||
@@ -583,7 +576,6 @@ where
|
||||
/// .run()
|
||||
/// .await
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
@@ -663,12 +655,27 @@ where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
_ = replace_blocks; // TODO
|
||||
handle_response(method, additional_context, app_fn, |app, chunks| {
|
||||
Box::pin(async move {
|
||||
Box::pin(app.to_html_stream_out_of_order().chain(chunks()))
|
||||
as PinnedStream<String>
|
||||
})
|
||||
})
|
||||
handle_response(
|
||||
method,
|
||||
additional_context,
|
||||
app_fn,
|
||||
|app, chunks, supports_ooo| {
|
||||
Box::pin(async move {
|
||||
let app = if cfg!(feature = "islands-router") {
|
||||
if supports_ooo {
|
||||
app.to_html_stream_out_of_order_branching()
|
||||
} else {
|
||||
app.to_html_stream_in_order_branching()
|
||||
}
|
||||
} else if supports_ooo {
|
||||
app.to_html_stream_out_of_order()
|
||||
} else {
|
||||
app.to_html_stream_in_order()
|
||||
};
|
||||
Box::pin(app.chain(chunks())) as PinnedStream<String>
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns an Actix [struct@Route](actix_web::Route) that listens for a `GET` request and tries
|
||||
@@ -694,12 +701,21 @@ pub fn render_app_to_stream_in_order_with_context<IV>(
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
handle_response(method, additional_context, app_fn, |app, chunks| {
|
||||
Box::pin(async move {
|
||||
Box::pin(app.to_html_stream_in_order().chain(chunks()))
|
||||
as PinnedStream<String>
|
||||
})
|
||||
})
|
||||
handle_response(
|
||||
method,
|
||||
additional_context,
|
||||
app_fn,
|
||||
|app, chunks, _supports_ooo| {
|
||||
Box::pin(async move {
|
||||
let app = if cfg!(feature = "islands-router") {
|
||||
app.to_html_stream_in_order_branching()
|
||||
} else {
|
||||
app.to_html_stream_in_order()
|
||||
};
|
||||
Box::pin(app.chain(chunks())) as PinnedStream<String>
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns an Actix [struct@Route](actix_web::Route) that listens for a `GET` request and tries
|
||||
@@ -731,12 +747,13 @@ where
|
||||
fn async_stream_builder<IV>(
|
||||
app: IV,
|
||||
chunks: BoxedFnOnce<PinnedStream<String>>,
|
||||
_supports_ooo: bool,
|
||||
) -> PinnedFuture<PinnedStream<String>>
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
Box::pin(async move {
|
||||
let app = if cfg!(feature = "dont-use-islands-router") {
|
||||
let app = if cfg!(feature = "islands-router") {
|
||||
app.to_html_stream_in_order_branching()
|
||||
} else {
|
||||
app.to_html_stream_in_order()
|
||||
@@ -776,6 +793,7 @@ fn leptos_corrected_path(req: &HttpRequest) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn handle_response<IV>(
|
||||
method: Method,
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
@@ -783,6 +801,7 @@ fn handle_response<IV>(
|
||||
stream_builder: fn(
|
||||
IV,
|
||||
BoxedFnOnce<PinnedStream<String>>,
|
||||
bool,
|
||||
) -> PinnedFuture<PinnedStream<String>>,
|
||||
) -> Route
|
||||
where
|
||||
@@ -793,6 +812,9 @@ where
|
||||
let add_context = additional_context.clone();
|
||||
|
||||
async move {
|
||||
let is_island_router_navigation = cfg!(feature = "islands-router")
|
||||
&& req.headers().get("Islands-Router").is_some();
|
||||
|
||||
let res_options = ResponseOptions::default();
|
||||
let (meta_context, meta_output) = ServerMetaContext::new();
|
||||
|
||||
@@ -803,6 +825,10 @@ where
|
||||
move || {
|
||||
provide_contexts(req, &meta_context, &res_options);
|
||||
add_context();
|
||||
|
||||
if is_island_router_navigation {
|
||||
provide_context(IslandsRouterNavigation);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -812,6 +838,7 @@ where
|
||||
additional_context,
|
||||
res_options,
|
||||
stream_builder,
|
||||
!is_island_router_navigation,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -1102,6 +1129,7 @@ impl StaticRouteGenerator {
|
||||
app_fn.clone(),
|
||||
additional_context,
|
||||
async_stream_builder,
|
||||
false,
|
||||
);
|
||||
|
||||
let sc = owner.shared_context().unwrap();
|
||||
|
||||
@@ -4,14 +4,14 @@ authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Axum integrations for the Leptos web framework."
|
||||
version = { workspace = true }
|
||||
version = "0.8.0-beta"
|
||||
rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
any_spawner = { workspace = true, features = ["tokio"] }
|
||||
hydration_context = { workspace = true }
|
||||
axum = { version = "0.7.9", default-features = false, features = [
|
||||
axum = { version = "0.8.1", default-features = false, features = [
|
||||
"matched-path",
|
||||
] }
|
||||
dashmap = "6"
|
||||
@@ -22,21 +22,28 @@ leptos_macro = { workspace = true, features = ["axum"] }
|
||||
leptos_meta = { workspace = true, features = ["ssr", "nonce"] }
|
||||
leptos_router = { workspace = true, features = ["ssr"] }
|
||||
leptos_integration_utils = { workspace = true }
|
||||
tachys = { workspace = true }
|
||||
once_cell = "1"
|
||||
parking_lot = "0.12.3"
|
||||
tokio = { version = "1.41", default-features = false }
|
||||
tokio = { version = "1.43", default-features = false }
|
||||
tower = { version = "0.5.1", features = ["util"] }
|
||||
tower-http = "0.6.2"
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
axum = "0.7.9"
|
||||
tokio = { version = "1.41", features = ["net", "rt-multi-thread"] }
|
||||
axum = "0.8.1"
|
||||
tokio = { version = "1.43", features = ["net", "rt-multi-thread"] }
|
||||
|
||||
[features]
|
||||
wasm = []
|
||||
default = ["tokio/fs", "tokio/sync", "tower-http/fs", "tower/util"]
|
||||
dont-use-islands-router = []
|
||||
default = [
|
||||
"tokio/fs",
|
||||
"tokio/sync",
|
||||
"tower-http/fs",
|
||||
"tower/util",
|
||||
"server_fn/axum",
|
||||
]
|
||||
islands-router = ["tachys/islands"]
|
||||
tracing = ["dep:tracing"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(missing_docs)]
|
||||
#![allow(clippy::type_complexity)]
|
||||
|
||||
//! Provides functions to easily integrate Leptos with Axum.
|
||||
//!
|
||||
@@ -278,12 +279,11 @@ pub fn generate_request_and_parts(
|
||||
///
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
///
|
||||
/// ```
|
||||
/// ```no_run
|
||||
/// use axum::{handler::Handler, routing::post, Router};
|
||||
/// use leptos::prelude::*;
|
||||
/// use std::net::SocketAddr;
|
||||
///
|
||||
/// # if false { // don't actually try to run a server in a doctest...
|
||||
/// #[cfg(feature = "default")]
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
@@ -299,7 +299,9 @@ pub fn generate_request_and_parts(
|
||||
/// .await
|
||||
/// .unwrap();
|
||||
/// }
|
||||
/// # }
|
||||
///
|
||||
/// # #[cfg(not(feature = "default"))]
|
||||
/// # fn main() { }
|
||||
/// ```
|
||||
/// Leptos provides a generic implementation of `handle_server_fns`. If access to more specific parts of the Request is desired,
|
||||
/// you can specify your own server fn handler based on this one and give it it's own route in the server macro.
|
||||
@@ -368,8 +370,6 @@ async fn handle_server_fns_inner(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
req: Request<Body>,
|
||||
) -> impl IntoResponse {
|
||||
use server_fn::middleware::Service;
|
||||
|
||||
let method = req.method().clone();
|
||||
let path = req.uri().path().to_string();
|
||||
let (req, parts) = generate_request_and_parts(req);
|
||||
@@ -442,7 +442,7 @@ pub type PinnedHtmlStream =
|
||||
/// to route it using [leptos_router], serving an HTML stream of your application.
|
||||
///
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
/// ```
|
||||
/// ```no_run
|
||||
/// use axum::{handler::Handler, Router};
|
||||
/// use leptos::{config::get_configuration, prelude::*};
|
||||
/// use std::{env, net::SocketAddr};
|
||||
@@ -452,7 +452,6 @@ pub type PinnedHtmlStream =
|
||||
/// view! { <main>"Hello, world!"</main> }
|
||||
/// }
|
||||
///
|
||||
/// # if false { // don't actually try to run a server in a doctest...
|
||||
/// #[cfg(feature = "default")]
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
@@ -471,7 +470,9 @@ pub type PinnedHtmlStream =
|
||||
/// .await
|
||||
/// .unwrap();
|
||||
/// }
|
||||
/// # }
|
||||
///
|
||||
/// # #[cfg(not(feature = "default"))]
|
||||
/// # fn main() { }
|
||||
/// ```
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
@@ -484,7 +485,7 @@ pub type PinnedHtmlStream =
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_to_stream<IV>(
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
) -> impl Fn(
|
||||
Request<Body>,
|
||||
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
|
||||
@@ -508,7 +509,7 @@ where
|
||||
)]
|
||||
pub fn render_route<S, IV>(
|
||||
paths: Vec<AxumRouteListing>,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
) -> impl Fn(
|
||||
State<S>,
|
||||
Request<Body>,
|
||||
@@ -530,7 +531,7 @@ where
|
||||
/// sending down its HTML. The app will become interactive once it has fully loaded.
|
||||
///
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
/// ```
|
||||
/// ```no_run
|
||||
/// use axum::{handler::Handler, Router};
|
||||
/// use leptos::{config::get_configuration, prelude::*};
|
||||
/// use std::{env, net::SocketAddr};
|
||||
@@ -540,7 +541,6 @@ where
|
||||
/// view! { <main>"Hello, world!"</main> }
|
||||
/// }
|
||||
///
|
||||
/// # if false { // don't actually try to run a server in a doctest...
|
||||
/// #[cfg(feature = "default")]
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
@@ -559,7 +559,9 @@ where
|
||||
/// .await
|
||||
/// .unwrap();
|
||||
/// }
|
||||
/// # }
|
||||
///
|
||||
/// # #[cfg(not(feature = "default"))]
|
||||
/// # fn main() { }
|
||||
/// ```
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
@@ -572,7 +574,7 @@ where
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_to_stream_in_order<IV>(
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
) -> impl Fn(
|
||||
Request<Body>,
|
||||
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
|
||||
@@ -625,13 +627,14 @@ where
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_to_stream_with_context<IV>(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
additional_context: impl Fn() + 'static + Clone + Send + Sync,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
) -> impl Fn(
|
||||
Request<Body>,
|
||||
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
|
||||
+ Clone
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
@@ -654,8 +657,8 @@ where
|
||||
)]
|
||||
pub fn render_route_with_context<S, IV>(
|
||||
paths: Vec<AxumRouteListing>,
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
additional_context: impl Fn() + 'static + Clone + Send + Sync,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
) -> impl Fn(
|
||||
State<S>,
|
||||
Request<Body>,
|
||||
@@ -756,25 +759,32 @@ where
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_to_stream_with_context_and_replace_blocks<IV>(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
additional_context: impl Fn() + 'static + Clone + Send + Sync,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
replace_blocks: bool,
|
||||
) -> impl Fn(
|
||||
Request<Body>,
|
||||
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
|
||||
+ Clone
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
_ = replace_blocks; // TODO
|
||||
handle_response(additional_context, app_fn, |app, chunks| {
|
||||
handle_response(additional_context, app_fn, |app, chunks, supports_ooo| {
|
||||
Box::pin(async move {
|
||||
let app = if cfg!(feature = "dont-use-islands-router") {
|
||||
app.to_html_stream_out_of_order_branching()
|
||||
} else {
|
||||
let app = if cfg!(feature = "islands-router") {
|
||||
if supports_ooo {
|
||||
app.to_html_stream_out_of_order_branching()
|
||||
} else {
|
||||
app.to_html_stream_in_order_branching()
|
||||
}
|
||||
} else if supports_ooo {
|
||||
app.to_html_stream_out_of_order()
|
||||
} else {
|
||||
app.to_html_stream_in_order()
|
||||
};
|
||||
Box::pin(app.chain(chunks())) as PinnedStream<String>
|
||||
})
|
||||
@@ -823,8 +833,8 @@ where
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_to_stream_in_order_with_context<IV>(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
additional_context: impl Fn() + 'static + Clone + Send + Sync,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
) -> impl Fn(
|
||||
Request<Body>,
|
||||
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
|
||||
@@ -834,8 +844,8 @@ pub fn render_app_to_stream_in_order_with_context<IV>(
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
handle_response(additional_context, app_fn, |app, chunks| {
|
||||
let app = if cfg!(feature = "dont-use-islands-router") {
|
||||
handle_response(additional_context, app_fn, |app, chunks, _supports_ooo| {
|
||||
let app = if cfg!(feature = "islands-router") {
|
||||
app.to_html_stream_in_order_branching()
|
||||
} else {
|
||||
app.to_html_stream_in_order()
|
||||
@@ -847,13 +857,18 @@ where
|
||||
}
|
||||
|
||||
fn handle_response<IV>(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
additional_context: impl Fn() + 'static + Clone + Send + Sync,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
stream_builder: fn(
|
||||
IV,
|
||||
BoxedFnOnce<PinnedStream<String>>,
|
||||
bool,
|
||||
) -> PinnedFuture<PinnedStream<String>>,
|
||||
) -> impl Fn(Request<Body>) -> PinnedFuture<Response<Body>> + Clone + Send + 'static
|
||||
) -> impl Fn(Request<Body>) -> PinnedFuture<Response<Body>>
|
||||
+ Clone
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
@@ -871,12 +886,16 @@ fn handle_response_inner<IV>(
|
||||
stream_builder: fn(
|
||||
IV,
|
||||
BoxedFnOnce<PinnedStream<String>>,
|
||||
bool,
|
||||
) -> PinnedFuture<PinnedStream<String>>,
|
||||
) -> PinnedFuture<Response<Body>>
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
Box::pin(async move {
|
||||
let is_island_router_navigation = cfg!(feature = "islands-router")
|
||||
&& req.headers().get("Islands-Router").is_some();
|
||||
|
||||
let add_context = additional_context.clone();
|
||||
let res_options = ResponseOptions::default();
|
||||
let (meta_context, meta_output) = ServerMetaContext::new();
|
||||
@@ -898,6 +917,10 @@ where
|
||||
res_options.clone(),
|
||||
);
|
||||
add_context();
|
||||
|
||||
if is_island_router_navigation {
|
||||
provide_context(IslandsRouterNavigation);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -907,6 +930,7 @@ where
|
||||
additional_context,
|
||||
res_options,
|
||||
stream_builder,
|
||||
!is_island_router_navigation,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -937,7 +961,7 @@ fn provide_contexts(
|
||||
/// `async` resources have loaded.
|
||||
///
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
/// ```
|
||||
/// ```no_run
|
||||
/// use axum::{handler::Handler, Router};
|
||||
/// use leptos::{config::get_configuration, prelude::*};
|
||||
/// use std::{env, net::SocketAddr};
|
||||
@@ -947,7 +971,6 @@ fn provide_contexts(
|
||||
/// view! { <main>"Hello, world!"</main> }
|
||||
/// }
|
||||
///
|
||||
/// # if false { // don't actually try to run a server in a doctest...
|
||||
/// #[cfg(feature = "default")]
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
@@ -967,7 +990,9 @@ fn provide_contexts(
|
||||
/// .await
|
||||
/// .unwrap();
|
||||
/// }
|
||||
/// # }
|
||||
///
|
||||
/// # #[cfg(not(feature = "default"))]
|
||||
/// # fn main() { }
|
||||
/// ```
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
@@ -980,7 +1005,7 @@ fn provide_contexts(
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_async<IV>(
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
) -> impl Fn(
|
||||
Request<Body>,
|
||||
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
|
||||
@@ -1034,8 +1059,8 @@ where
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_async_stream_with_context<IV>(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
additional_context: impl Fn() + 'static + Clone + Send + Sync,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
) -> impl Fn(
|
||||
Request<Body>,
|
||||
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
|
||||
@@ -1045,9 +1070,9 @@ pub fn render_app_async_stream_with_context<IV>(
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
handle_response(additional_context, app_fn, |app, chunks| {
|
||||
handle_response(additional_context, app_fn, |app, chunks, _supports_ooo| {
|
||||
Box::pin(async move {
|
||||
let app = if cfg!(feature = "dont-use-islands-router") {
|
||||
let app = if cfg!(feature = "islands-router") {
|
||||
app.to_html_stream_in_order_branching()
|
||||
} else {
|
||||
app.to_html_stream_in_order()
|
||||
@@ -1101,8 +1126,8 @@ where
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_async_with_context<IV>(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
additional_context: impl Fn() + 'static + Clone + Send + Sync,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
) -> impl Fn(
|
||||
Request<Body>,
|
||||
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
|
||||
@@ -1118,12 +1143,13 @@ where
|
||||
fn async_stream_builder<IV>(
|
||||
app: IV,
|
||||
chunks: BoxedFnOnce<PinnedStream<String>>,
|
||||
_supports_ooo: bool,
|
||||
) -> PinnedFuture<PinnedStream<String>>
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
Box::pin(async move {
|
||||
let app = if cfg!(feature = "dont-use-islands-router") {
|
||||
let app = if cfg!(feature = "islands-router") {
|
||||
app.to_html_stream_in_order_branching()
|
||||
} else {
|
||||
app.to_html_stream_in_order()
|
||||
@@ -1396,6 +1422,7 @@ impl StaticRouteGenerator {
|
||||
app_fn.clone(),
|
||||
additional_context,
|
||||
async_stream_builder,
|
||||
false,
|
||||
);
|
||||
|
||||
let sc = owner.shared_context().unwrap();
|
||||
@@ -1641,7 +1668,7 @@ where
|
||||
self,
|
||||
options: &S,
|
||||
paths: Vec<AxumRouteListing>,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
IV: IntoView + 'static;
|
||||
@@ -1656,8 +1683,8 @@ where
|
||||
self,
|
||||
options: &S,
|
||||
paths: Vec<AxumRouteListing>,
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
additional_context: impl Fn() + 'static + Clone + Send + Sync,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
IV: IntoView + 'static;
|
||||
@@ -1690,12 +1717,15 @@ impl AxumPath for Vec<PathSegment> {
|
||||
match segment {
|
||||
PathSegment::Static(s) => path.push_str(s),
|
||||
PathSegment::Param(s) => {
|
||||
path.push(':');
|
||||
path.push('{');
|
||||
path.push_str(s);
|
||||
path.push('}');
|
||||
}
|
||||
PathSegment::Splat(s) => {
|
||||
path.push('{');
|
||||
path.push('*');
|
||||
path.push_str(s);
|
||||
path.push('}');
|
||||
}
|
||||
PathSegment::Unit => {}
|
||||
PathSegment::OptionalParam(_) => {
|
||||
@@ -1727,7 +1757,7 @@ where
|
||||
self,
|
||||
state: &S,
|
||||
paths: Vec<AxumRouteListing>,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
@@ -1743,8 +1773,8 @@ where
|
||||
self,
|
||||
state: &S,
|
||||
paths: Vec<AxumRouteListing>,
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
additional_context: impl Fn() + 'static + Clone + Send + Sync,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
@@ -1825,64 +1855,64 @@ where
|
||||
}
|
||||
} else {
|
||||
router.route(
|
||||
path,
|
||||
match listing.mode() {
|
||||
SsrMode::OutOfOrder => {
|
||||
let s = render_app_to_stream_with_context(
|
||||
cx_with_state_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
);
|
||||
match method {
|
||||
leptos_router::Method::Get => get(s),
|
||||
leptos_router::Method::Post => post(s),
|
||||
leptos_router::Method::Put => put(s),
|
||||
leptos_router::Method::Delete => delete(s),
|
||||
leptos_router::Method::Patch => patch(s),
|
||||
path,
|
||||
match listing.mode() {
|
||||
SsrMode::OutOfOrder => {
|
||||
let s = render_app_to_stream_with_context(
|
||||
cx_with_state_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
);
|
||||
match method {
|
||||
leptos_router::Method::Get => get(s),
|
||||
leptos_router::Method::Post => post(s),
|
||||
leptos_router::Method::Put => put(s),
|
||||
leptos_router::Method::Delete => delete(s),
|
||||
leptos_router::Method::Patch => patch(s),
|
||||
}
|
||||
}
|
||||
}
|
||||
SsrMode::PartiallyBlocked => {
|
||||
let s = render_app_to_stream_with_context_and_replace_blocks(
|
||||
cx_with_state_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
true
|
||||
);
|
||||
match method {
|
||||
leptos_router::Method::Get => get(s),
|
||||
leptos_router::Method::Post => post(s),
|
||||
leptos_router::Method::Put => put(s),
|
||||
leptos_router::Method::Delete => delete(s),
|
||||
leptos_router::Method::Patch => patch(s),
|
||||
SsrMode::PartiallyBlocked => {
|
||||
let s = render_app_to_stream_with_context_and_replace_blocks(
|
||||
cx_with_state_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
true
|
||||
);
|
||||
match method {
|
||||
leptos_router::Method::Get => get(s),
|
||||
leptos_router::Method::Post => post(s),
|
||||
leptos_router::Method::Put => put(s),
|
||||
leptos_router::Method::Delete => delete(s),
|
||||
leptos_router::Method::Patch => patch(s),
|
||||
}
|
||||
}
|
||||
}
|
||||
SsrMode::InOrder => {
|
||||
let s = render_app_to_stream_in_order_with_context(
|
||||
cx_with_state_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
);
|
||||
match method {
|
||||
leptos_router::Method::Get => get(s),
|
||||
leptos_router::Method::Post => post(s),
|
||||
leptos_router::Method::Put => put(s),
|
||||
leptos_router::Method::Delete => delete(s),
|
||||
leptos_router::Method::Patch => patch(s),
|
||||
SsrMode::InOrder => {
|
||||
let s = render_app_to_stream_in_order_with_context(
|
||||
cx_with_state_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
);
|
||||
match method {
|
||||
leptos_router::Method::Get => get(s),
|
||||
leptos_router::Method::Post => post(s),
|
||||
leptos_router::Method::Put => put(s),
|
||||
leptos_router::Method::Delete => delete(s),
|
||||
leptos_router::Method::Patch => patch(s),
|
||||
}
|
||||
}
|
||||
}
|
||||
SsrMode::Async => {
|
||||
let s = render_app_async_with_context(
|
||||
cx_with_state_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
);
|
||||
match method {
|
||||
leptos_router::Method::Get => get(s),
|
||||
leptos_router::Method::Post => post(s),
|
||||
leptos_router::Method::Put => put(s),
|
||||
leptos_router::Method::Delete => delete(s),
|
||||
leptos_router::Method::Patch => patch(s),
|
||||
SsrMode::Async => {
|
||||
let s = render_app_async_with_context(
|
||||
cx_with_state_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
);
|
||||
match method {
|
||||
leptos_router::Method::Get => get(s),
|
||||
leptos_router::Method::Post => post(s),
|
||||
leptos_router::Method::Put => put(s),
|
||||
leptos_router::Method::Delete => delete(s),
|
||||
leptos_router::Method::Patch => patch(s),
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => unreachable!()
|
||||
},
|
||||
)
|
||||
_ => unreachable!()
|
||||
},
|
||||
)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1983,7 +2013,7 @@ where
|
||||
#[cfg(feature = "default")]
|
||||
pub fn file_and_error_handler_with_context<S, IV>(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
shell: fn(LeptosOptions) -> IV,
|
||||
shell: impl Fn(LeptosOptions) -> IV + 'static + Clone + Send,
|
||||
) -> impl Fn(
|
||||
Uri,
|
||||
State<S>,
|
||||
@@ -2000,6 +2030,7 @@ where
|
||||
move |uri: Uri, State(state): State<S>, req: Request<Body>| {
|
||||
Box::pin({
|
||||
let additional_context = additional_context.clone();
|
||||
let shell = shell.clone();
|
||||
async move {
|
||||
let options = LeptosOptions::from_ref(&state);
|
||||
let res =
|
||||
@@ -2016,7 +2047,7 @@ where
|
||||
},
|
||||
move || shell(options),
|
||||
req,
|
||||
|app, chunks| {
|
||||
|app, chunks, _supports_ooo| {
|
||||
Box::pin(async move {
|
||||
let app = app
|
||||
.to_html_stream_in_order()
|
||||
@@ -2043,7 +2074,7 @@ where
|
||||
/// simply reuse the source code of this function in your own application.
|
||||
#[cfg(feature = "default")]
|
||||
pub fn file_and_error_handler<S, IV>(
|
||||
shell: fn(LeptosOptions) -> IV,
|
||||
shell: impl Fn(LeptosOptions) -> IV + 'static + Clone + Send,
|
||||
) -> impl Fn(
|
||||
Uri,
|
||||
State<S>,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(clippy::type_complexity)]
|
||||
|
||||
use futures::{stream::once, Stream, StreamExt};
|
||||
use hydration_context::{SharedContext, SsrSharedContext};
|
||||
use leptos::{
|
||||
@@ -31,14 +33,20 @@ pub trait ExtendResponse: Sized {
|
||||
stream_builder: fn(
|
||||
IV,
|
||||
BoxedFnOnce<PinnedStream<String>>,
|
||||
bool,
|
||||
) -> PinnedFuture<PinnedStream<String>>,
|
||||
supports_ooo: bool,
|
||||
) -> impl Future<Output = Self> + Send
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
async move {
|
||||
let (owner, stream) =
|
||||
build_response(app_fn, additional_context, stream_builder);
|
||||
let (owner, stream) = build_response(
|
||||
app_fn,
|
||||
additional_context,
|
||||
stream_builder,
|
||||
supports_ooo,
|
||||
);
|
||||
|
||||
let sc = owner.shared_context().unwrap();
|
||||
|
||||
@@ -94,7 +102,11 @@ pub fn build_response<IV>(
|
||||
stream_builder: fn(
|
||||
IV,
|
||||
BoxedFnOnce<PinnedStream<String>>,
|
||||
// this argument indicates whether a request wants to support out-of-order streaming
|
||||
// responses
|
||||
bool,
|
||||
) -> PinnedFuture<PinnedStream<String>>,
|
||||
is_islands_router_navigation: bool,
|
||||
) -> (Owner, PinnedFuture<PinnedStream<String>>)
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
@@ -138,7 +150,7 @@ where
|
||||
//
|
||||
// we also don't actually start hydrating until after the whole stream is complete,
|
||||
// so it's not useful to send those scripts down earlier.
|
||||
stream_builder(app, chunks)
|
||||
stream_builder(app, chunks, is_islands_router_navigation)
|
||||
});
|
||||
|
||||
stream.await
|
||||
|
||||
@@ -42,22 +42,17 @@ typed-builder = "0.20.0"
|
||||
typed-builder-macro = "0.20.0"
|
||||
serde = "1.0"
|
||||
serde_json = { version = "1.0", optional = true }
|
||||
server_fn = { workspace = true, features = [
|
||||
"form-redirects",
|
||||
"browser",
|
||||
"url",
|
||||
] }
|
||||
server_fn = { workspace = true, features = ["form-redirects", "browser"] }
|
||||
web-sys = { version = "0.3.72", features = [
|
||||
"ShadowRoot",
|
||||
"ShadowRootInit",
|
||||
"ShadowRootMode",
|
||||
] }
|
||||
wasm-bindgen = "0.2.97"
|
||||
wasm-bindgen = { workspace = true }
|
||||
serde_qs = "0.13.0"
|
||||
slotmap = "1.0"
|
||||
futures = "0.3.31"
|
||||
send_wrapper = "0.6.0"
|
||||
getrandom = { version = "0.2", features = ["js"], optional = true }
|
||||
|
||||
[features]
|
||||
hydration = [
|
||||
@@ -66,13 +61,12 @@ hydration = [
|
||||
"hydration_context/browser",
|
||||
"leptos_dom/hydration",
|
||||
]
|
||||
csr = ["leptos_macro/csr", "reactive_graph/effects", "dep:getrandom"]
|
||||
csr = ["leptos_macro/csr", "reactive_graph/effects"]
|
||||
hydrate = [
|
||||
"leptos_macro/hydrate",
|
||||
"hydration",
|
||||
"tachys/hydrate",
|
||||
"reactive_graph/effects",
|
||||
"dep:getrandom",
|
||||
]
|
||||
default-tls = ["server_fn/default-tls"]
|
||||
rustls = ["server_fn/rustls"]
|
||||
@@ -84,10 +78,7 @@ ssr = [
|
||||
"tachys/ssr",
|
||||
]
|
||||
nightly = ["leptos_macro/nightly", "reactive_graph/nightly", "tachys/nightly"]
|
||||
rkyv = [
|
||||
"server_fn/rkyv",
|
||||
"leptos_server/rkyv"
|
||||
]
|
||||
rkyv = ["server_fn/rkyv", "leptos_server/rkyv"]
|
||||
tracing = [
|
||||
"dep:tracing",
|
||||
"reactive_graph/tracing",
|
||||
@@ -105,6 +96,15 @@ trace-component-props = [
|
||||
]
|
||||
delegation = ["tachys/delegation"]
|
||||
|
||||
# Having an erasure feature rather than normal --cfg erase_components for the proc macro crate is a workaround for this rust issue:
|
||||
# https://github.com/rust-lang/cargo/issues/4423
|
||||
# TLDR proc macros will ignore RUSTFLAGS when --target is specified on the cargo command.
|
||||
# This works around the issue by the non proc-macro crate which does see RUSTFLAGS enabling the replacement feature on the proc-macro crate, which wouldn't.
|
||||
# This is automatic as long as the leptos crate is depended upon,
|
||||
# downstream usage should never manually enable this feature.
|
||||
[target.'cfg(erase_components)'.dependencies]
|
||||
leptos_macro = { workspace = true, features = ["__internal_erase_components"] }
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = [
|
||||
"nightly",
|
||||
|
||||
@@ -43,7 +43,7 @@ pub fn AttributeInterceptor<Chil, T>(
|
||||
) -> impl IntoView
|
||||
where
|
||||
Chil: Fn(AnyAttribute) -> T + Send + Sync + 'static,
|
||||
T: IntoView,
|
||||
T: IntoView + 'static,
|
||||
{
|
||||
AttributeInterceptorInner::new(children)
|
||||
}
|
||||
@@ -86,7 +86,7 @@ impl<T: IntoView, A: Attribute> Render for AttributeInterceptorInner<T, A> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IntoView, A> AddAnyAttr for AttributeInterceptorInner<T, A>
|
||||
impl<T: IntoView + 'static, A> AddAnyAttr for AttributeInterceptorInner<T, A>
|
||||
where
|
||||
A: Attribute,
|
||||
{
|
||||
@@ -114,8 +114,11 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IntoView, A: Attribute> RenderHtml for AttributeInterceptorInner<T, A> {
|
||||
impl<T: IntoView + 'static, A: Attribute> RenderHtml
|
||||
for AttributeInterceptorInner<T, A>
|
||||
{
|
||||
type AsyncOutput = T::AsyncOutput;
|
||||
type Owned = AttributeInterceptorInner<T, A::CloneableOwned>;
|
||||
|
||||
const MIN_LENGTH: usize = T::MIN_LENGTH;
|
||||
|
||||
@@ -135,9 +138,15 @@ impl<T: IntoView, A: Attribute> RenderHtml for AttributeInterceptorInner<T, A> {
|
||||
position: &mut leptos::tachys::view::Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
_extra_attrs: Vec<AnyAttribute>,
|
||||
) {
|
||||
self.children
|
||||
.to_html_with_buf(buf, position, escape, mark_branches)
|
||||
self.children.to_html_with_buf(
|
||||
buf,
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
vec![],
|
||||
)
|
||||
}
|
||||
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
@@ -147,4 +156,12 @@ impl<T: IntoView, A: Attribute> RenderHtml for AttributeInterceptorInner<T, A> {
|
||||
) -> Self::State {
|
||||
self.children.hydrate::<FROM_SERVER>(cursor, position)
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
AttributeInterceptorInner {
|
||||
children_builder: self.children_builder,
|
||||
children: self.children,
|
||||
attributes: self.attributes.into_cloneable_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,13 +43,20 @@
|
||||
|
||||
use reactive_graph::{
|
||||
owner::{LocalStorage, StoredValue},
|
||||
traits::WithValue,
|
||||
traits::{Dispose, WithValue},
|
||||
};
|
||||
use std::{fmt, rc::Rc, sync::Arc};
|
||||
|
||||
/// A wrapper trait for calling callbacks.
|
||||
pub trait Callable<In: 'static, Out: 'static = ()> {
|
||||
/// calls the callback with the specified argument.
|
||||
///
|
||||
/// Returns None if the callback has been disposed
|
||||
fn try_run(&self, input: In) -> Option<Out>;
|
||||
/// calls the callback with the specified argument.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if you try to run a callback that has been disposed
|
||||
fn run(&self, input: In) -> Out;
|
||||
}
|
||||
|
||||
@@ -72,6 +79,12 @@ impl<In, Out> Clone for UnsyncCallback<In, Out> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<In, Out> Dispose for UnsyncCallback<In, Out> {
|
||||
fn dispose(self) {
|
||||
self.0.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
impl<In, Out> UnsyncCallback<In, Out> {
|
||||
/// Creates a new callback from the given function.
|
||||
pub fn new<F>(f: F) -> UnsyncCallback<In, Out>
|
||||
@@ -93,6 +106,10 @@ impl<In, Out> UnsyncCallback<In, Out> {
|
||||
}
|
||||
|
||||
impl<In: 'static, Out: 'static> Callable<In, Out> for UnsyncCallback<In, Out> {
|
||||
fn try_run(&self, input: In) -> Option<Out> {
|
||||
self.0.try_with_value(|fun| fun(input))
|
||||
}
|
||||
|
||||
fn run(&self, input: In) -> Out {
|
||||
self.0.with_value(|fun| fun(input))
|
||||
}
|
||||
@@ -168,10 +185,12 @@ impl<In, Out> fmt::Debug for Callback<In, Out> {
|
||||
}
|
||||
|
||||
impl<In, Out> Callable<In, Out> for Callback<In, Out> {
|
||||
fn try_run(&self, input: In) -> Option<Out> {
|
||||
self.0.try_with_value(|fun| fun(input))
|
||||
}
|
||||
|
||||
fn run(&self, input: In) -> Out {
|
||||
self.0
|
||||
.try_with_value(|f| f(input))
|
||||
.expect("called a callback that has been disposed")
|
||||
self.0.with_value(|f| f(input))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,6 +200,12 @@ impl<In, Out> Clone for Callback<In, Out> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<In, Out> Dispose for Callback<In, Out> {
|
||||
fn dispose(self) {
|
||||
self.0.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
impl<In, Out> Copy for Callback<In, Out> {}
|
||||
|
||||
macro_rules! impl_callable_from_fn {
|
||||
@@ -239,7 +264,9 @@ impl<In: 'static, Out: 'static> Callback<In, Out> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Callable;
|
||||
use crate::callback::{Callback, UnsyncCallback};
|
||||
use reactive_graph::traits::Dispose;
|
||||
|
||||
struct NoClone {}
|
||||
|
||||
@@ -270,10 +297,26 @@ mod tests {
|
||||
(|num, s| format!("{num} {s}")).into();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_callback_try_run() {
|
||||
let callback = Callback::new(move |arg| arg);
|
||||
assert_eq!(callback.try_run((0,)), Some((0,)));
|
||||
callback.dispose();
|
||||
assert_eq!(callback.try_run((0,)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsync_callback_try_run() {
|
||||
let callback = UnsyncCallback::new(move |arg| arg);
|
||||
assert_eq!(callback.try_run((0,)), Some((0,)));
|
||||
callback.dispose();
|
||||
assert_eq!(callback.try_run((0,)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn callback_matches_same() {
|
||||
let callback1 = Callback::new(|x: i32| x * 2);
|
||||
let callback2 = callback1.clone();
|
||||
let callback2 = callback1;
|
||||
assert!(callback1.matches(&callback2));
|
||||
}
|
||||
|
||||
@@ -287,7 +330,7 @@ mod tests {
|
||||
#[test]
|
||||
fn unsync_callback_matches_same() {
|
||||
let callback1 = UnsyncCallback::new(|x: i32| x * 2);
|
||||
let callback2 = callback1.clone();
|
||||
let callback2 = callback1;
|
||||
assert!(callback1.matches(&callback2));
|
||||
}
|
||||
|
||||
|
||||
@@ -91,6 +91,9 @@ pub trait ToChildren<F> {
|
||||
fn to_children(f: F) -> Self;
|
||||
}
|
||||
|
||||
/// Compiler optimisation, can be used with certain type to avoid unique closures in the view!{} macro.
|
||||
pub struct ChildrenOptContainer<T>(pub T);
|
||||
|
||||
impl<F, C> ToChildren<F> for Children
|
||||
where
|
||||
F: FnOnce() -> C + Send + 'static,
|
||||
@@ -102,6 +105,16 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ToChildren<ChildrenOptContainer<T>> for Children
|
||||
where
|
||||
T: IntoAny + Send + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn to_children(t: ChildrenOptContainer<T>) -> Self {
|
||||
Box::new(move || t.0.into_any())
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, C> ToChildren<F> for ChildrenFn
|
||||
where
|
||||
F: Fn() -> C + Send + Sync + 'static,
|
||||
@@ -113,6 +126,16 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ToChildren<ChildrenOptContainer<T>> for ChildrenFn
|
||||
where
|
||||
T: IntoAny + Clone + Send + Sync + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn to_children(t: ChildrenOptContainer<T>) -> Self {
|
||||
Arc::new(move || t.0.clone().into_any())
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, C> ToChildren<F> for ChildrenFnMut
|
||||
where
|
||||
F: Fn() -> C + Send + 'static,
|
||||
@@ -124,6 +147,16 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ToChildren<ChildrenOptContainer<T>> for ChildrenFnMut
|
||||
where
|
||||
T: IntoAny + Clone + Send + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn to_children(t: ChildrenOptContainer<T>) -> Self {
|
||||
Box::new(move || t.0.clone().into_any())
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, C> ToChildren<F> for BoxedChildrenFn
|
||||
where
|
||||
F: Fn() -> C + Send + 'static,
|
||||
@@ -135,6 +168,16 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ToChildren<ChildrenOptContainer<T>> for BoxedChildrenFn
|
||||
where
|
||||
T: IntoAny + Clone + Send + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn to_children(t: ChildrenOptContainer<T>) -> Self {
|
||||
Box::new(move || t.0.clone().into_any())
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, C> ToChildren<F> for ChildrenFragment
|
||||
where
|
||||
F: FnOnce() -> C + Send + 'static,
|
||||
@@ -146,6 +189,16 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ToChildren<ChildrenOptContainer<T>> for ChildrenFragment
|
||||
where
|
||||
T: IntoAny + Send + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn to_children(t: ChildrenOptContainer<T>) -> Self {
|
||||
Box::new(move || Fragment::new(vec![t.0.into_any()]))
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, C> ToChildren<F> for ChildrenFragmentFn
|
||||
where
|
||||
F: Fn() -> C + Send + 'static,
|
||||
@@ -157,6 +210,16 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ToChildren<ChildrenOptContainer<T>> for ChildrenFragmentFn
|
||||
where
|
||||
T: IntoAny + Clone + Send + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn to_children(t: ChildrenOptContainer<T>) -> Self {
|
||||
Arc::new(move || Fragment::new(vec![t.0.clone().into_any()]))
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, C> ToChildren<F> for ChildrenFragmentMut
|
||||
where
|
||||
F: FnMut() -> C + Send + 'static,
|
||||
@@ -168,6 +231,16 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ToChildren<ChildrenOptContainer<T>> for ChildrenFragmentMut
|
||||
where
|
||||
T: IntoAny + Clone + Send + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn to_children(t: ChildrenOptContainer<T>) -> Self {
|
||||
Box::new(move || Fragment::new(vec![t.0.clone().into_any()]))
|
||||
}
|
||||
}
|
||||
|
||||
/// New-type wrapper for a function that returns a view with `From` and `Default` traits implemented
|
||||
/// to enable optional props in for example `<Show>` and `<Suspense>`.
|
||||
#[derive(Clone)]
|
||||
@@ -246,6 +319,16 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ToChildren<ChildrenOptContainer<T>> for TypedChildren<T>
|
||||
where
|
||||
T: IntoView + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn to_children(t: ChildrenOptContainer<T>) -> Self {
|
||||
TypedChildren(Box::new(move || t.0.into_view()))
|
||||
}
|
||||
}
|
||||
|
||||
/// A typed equivalent to [`ChildrenFnMut`], which takes a generic but preserves type information to
|
||||
/// allow the compiler to optimize the view more effectively.
|
||||
pub struct TypedChildrenMut<T>(Box<dyn FnMut() -> View<T> + Send>);
|
||||
@@ -275,6 +358,16 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ToChildren<ChildrenOptContainer<T>> for TypedChildrenMut<T>
|
||||
where
|
||||
T: IntoView + Clone + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn to_children(t: ChildrenOptContainer<T>) -> Self {
|
||||
TypedChildrenMut(Box::new(move || t.0.clone().into_view()))
|
||||
}
|
||||
}
|
||||
|
||||
/// A typed equivalent to [`ChildrenFn`], which takes a generic but preserves type information to
|
||||
/// allow the compiler to optimize the view more effectively.
|
||||
pub struct TypedChildrenFn<T>(Arc<dyn Fn() -> View<T> + Send + Sync>);
|
||||
@@ -310,3 +403,13 @@ where
|
||||
TypedChildrenFn(Arc::new(move || f().into_view()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ToChildren<ChildrenOptContainer<T>> for TypedChildrenFn<T>
|
||||
where
|
||||
T: IntoView + Clone + Sync + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn to_children(t: ChildrenOptContainer<T>) -> Self {
|
||||
TypedChildrenFn(Arc::new(move || t.0.clone().into_view()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use reactive_graph::{
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
use tachys::{
|
||||
html::attribute::Attribute,
|
||||
html::attribute::{any_attribute::AnyAttribute, Attribute},
|
||||
hydration::Cursor,
|
||||
reactive_graph::OwnedView,
|
||||
ssr::StreamBuilder,
|
||||
@@ -163,6 +163,14 @@ where
|
||||
self.children.insert_before_this(child)
|
||||
}
|
||||
}
|
||||
|
||||
fn elements(&self) -> Vec<tachys::renderer::types::Element> {
|
||||
if let Some(fallback) = &self.fallback {
|
||||
fallback.elements()
|
||||
} else {
|
||||
self.children.elements()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Chil, FalFn, Fal> Render for ErrorBoundaryView<Chil, FalFn>
|
||||
@@ -268,6 +276,7 @@ where
|
||||
Fal: RenderHtml + Send + 'static,
|
||||
{
|
||||
type AsyncOutput = ErrorBoundaryView<Chil::AsyncOutput, FalFn>;
|
||||
type Owned = Self;
|
||||
|
||||
const MIN_LENGTH: usize = Chil::MIN_LENGTH;
|
||||
|
||||
@@ -301,6 +310,7 @@ where
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) {
|
||||
// first, attempt to serialize the children to HTML, then check for errors
|
||||
let _hook = throw_error::set_error_hook(self.hook);
|
||||
@@ -311,6 +321,7 @@ where
|
||||
&mut new_pos,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs.clone(),
|
||||
);
|
||||
|
||||
// any thrown errors would've been caught here
|
||||
@@ -323,6 +334,7 @@ where
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -333,6 +345,7 @@ where
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
@@ -345,6 +358,7 @@ where
|
||||
&mut new_pos,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs.clone(),
|
||||
);
|
||||
|
||||
// any thrown errors would've been caught here
|
||||
@@ -358,6 +372,7 @@ where
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
buf.push_sync(&fallback);
|
||||
}
|
||||
@@ -423,6 +438,10 @@ where
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -6,7 +6,10 @@ use reactive_graph::{
|
||||
traits::Set,
|
||||
};
|
||||
use std::hash::Hash;
|
||||
use tachys::{reactive_graph::OwnedView, view::keyed::keyed};
|
||||
use tachys::{
|
||||
reactive_graph::OwnedView,
|
||||
view::keyed::{keyed, SerializableKey},
|
||||
};
|
||||
|
||||
/// Iterates over children and displays them, keyed by the `key` function given.
|
||||
///
|
||||
@@ -121,7 +124,7 @@ where
|
||||
EF: Fn(T) -> N + Send + Clone + 'static,
|
||||
N: IntoView + 'static,
|
||||
KF: Fn(&T) -> K + Send + Clone + 'static,
|
||||
K: Eq + Hash + 'static,
|
||||
K: Eq + Hash + SerializableKey + 'static,
|
||||
T: Send + 'static,
|
||||
{
|
||||
// this takes the owner of the For itself
|
||||
@@ -195,7 +198,7 @@ where
|
||||
EF: Fn(ReadSignal<usize>, T) -> N + Send + Clone + 'static,
|
||||
N: IntoView + 'static,
|
||||
KF: Fn(&T) -> K + Send + Clone + 'static,
|
||||
K: Eq + Hash + 'static,
|
||||
K: Eq + Hash + SerializableKey + 'static,
|
||||
T: Send + 'static,
|
||||
{
|
||||
// this takes the owner of the For itself
|
||||
@@ -218,6 +221,7 @@ where
|
||||
};
|
||||
move || keyed(each(), key.clone(), children.clone())
|
||||
}
|
||||
|
||||
/*
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
@@ -3,7 +3,11 @@ use leptos_dom::helpers::window;
|
||||
use leptos_server::{ServerAction, ServerMultiAction};
|
||||
use serde::de::DeserializeOwned;
|
||||
use server_fn::{
|
||||
client::Client, codec::PostUrl, request::ClientReq, ServerFn, ServerFnError,
|
||||
client::Client,
|
||||
codec::PostUrl,
|
||||
error::{IntoAppError, ServerFnErrorErr},
|
||||
request::ClientReq,
|
||||
Http, ServerFn,
|
||||
};
|
||||
use tachys::{
|
||||
either::Either,
|
||||
@@ -71,7 +75,7 @@ use web_sys::{
|
||||
/// ```
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
|
||||
#[component]
|
||||
pub fn ActionForm<ServFn>(
|
||||
pub fn ActionForm<ServFn, OutputProtocol>(
|
||||
/// The action from which to build the form.
|
||||
action: ServerAction<ServFn>,
|
||||
/// A [`NodeRef`] in which the `<form>` element should be stored.
|
||||
@@ -82,7 +86,7 @@ pub fn ActionForm<ServFn>(
|
||||
) -> impl IntoView
|
||||
where
|
||||
ServFn: DeserializeOwned
|
||||
+ ServerFn<InputEncoding = PostUrl>
|
||||
+ ServerFn<Protocol = Http<PostUrl, OutputProtocol>>
|
||||
+ Clone
|
||||
+ Send
|
||||
+ Sync
|
||||
@@ -121,9 +125,10 @@ where
|
||||
"Error converting form field into server function \
|
||||
arguments: {err:?}"
|
||||
);
|
||||
value.set(Some(Err(ServerFnError::Serialization(
|
||||
value.set(Some(Err(ServerFnErrorErr::Serialization(
|
||||
err.to_string(),
|
||||
))));
|
||||
)
|
||||
.into_app_error())));
|
||||
version.update(|n| *n += 1);
|
||||
}
|
||||
}
|
||||
@@ -146,7 +151,7 @@ where
|
||||
/// [`form`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form)
|
||||
/// progressively enhanced to use client-side routing.
|
||||
#[component]
|
||||
pub fn MultiActionForm<ServFn>(
|
||||
pub fn MultiActionForm<ServFn, OutputProtocol>(
|
||||
/// The action from which to build the form.
|
||||
action: ServerMultiAction<ServFn>,
|
||||
/// A [`NodeRef`] in which the `<form>` element should be stored.
|
||||
@@ -160,7 +165,7 @@ where
|
||||
+ Sync
|
||||
+ Clone
|
||||
+ DeserializeOwned
|
||||
+ ServerFn<InputEncoding = PostUrl>
|
||||
+ ServerFn<Protocol = Http<PostUrl, OutputProtocol>>
|
||||
+ 'static,
|
||||
ServFn::Output: Send + Sync + 'static,
|
||||
<<ServFn::Client as Client<ServFn::Error>>::Request as ClientReq<
|
||||
@@ -187,9 +192,10 @@ where
|
||||
action.dispatch(new_input);
|
||||
}
|
||||
Err(err) => {
|
||||
action.dispatch_sync(Err(ServerFnError::Serialization(
|
||||
action.dispatch_sync(Err(ServerFnErrorErr::Serialization(
|
||||
err.to_string(),
|
||||
)));
|
||||
)
|
||||
.into_app_error()));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -52,6 +52,8 @@
|
||||
mod.hydrate();
|
||||
hydrateIslands(document.body, mod);
|
||||
});
|
||||
|
||||
window.__hydrateIsland = (el, id) => hydrateIsland(el, id, mod);
|
||||
})
|
||||
});
|
||||
})
|
||||
|
||||
378
leptos/src/hydration/islands_routing.js
Normal file
378
leptos/src/hydration/islands_routing.js
Normal file
@@ -0,0 +1,378 @@
|
||||
let NAVIGATION = 0;
|
||||
|
||||
window.addEventListener("click", async (ev) => {
|
||||
const req = clickToReq(ev);
|
||||
if(!req) {
|
||||
return;
|
||||
}
|
||||
|
||||
ev.preventDefault();
|
||||
await navigateToPage(req, true);
|
||||
});
|
||||
|
||||
window.addEventListener("popstate", async (ev) => {
|
||||
const req = new Request(window.location);
|
||||
ev.preventDefault();
|
||||
await navigateToPage(req, true, true);
|
||||
});
|
||||
|
||||
window.addEventListener("submit", async (ev) => {
|
||||
const req = submitToReq(ev);
|
||||
if(!req) {
|
||||
return;
|
||||
}
|
||||
|
||||
ev.preventDefault();
|
||||
await navigateToPage(req, true);
|
||||
});
|
||||
|
||||
async function navigateToPage(
|
||||
/** @type Request */
|
||||
req,
|
||||
/** @type bool */
|
||||
useViewTransition,
|
||||
/** @type bool */
|
||||
replace
|
||||
) {
|
||||
NAVIGATION += 1;
|
||||
const currentNav = NAVIGATION;
|
||||
|
||||
// add a custom header to indicate that we're on a subsequent navigation
|
||||
req.headers.append("Islands-Router", "true");
|
||||
|
||||
// fetch the new page
|
||||
const resp = await fetch(req);
|
||||
const redirected = resp.redirected;
|
||||
const htmlString = await resp.text();
|
||||
|
||||
if(NAVIGATION === currentNav) {
|
||||
// The 'doc' variable now contains the parsed DOM
|
||||
const transition = async () => {
|
||||
try {
|
||||
diffPages(htmlString);
|
||||
for(const island of document.querySelectorAll("leptos-island")) {
|
||||
if(!island.$$hydrated) {
|
||||
__hydrateIsland(island, island.dataset.component);
|
||||
island.$$hydrated = true;
|
||||
}
|
||||
}
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
// Not all browsers support startViewTransition; see https://caniuse.com/?search=startViewTransition
|
||||
if (useViewTransition && document.startViewTransition) {
|
||||
await document.startViewTransition(transition);
|
||||
} else {
|
||||
await transition()
|
||||
}
|
||||
|
||||
const url = redirected ? resp.url : req.url;
|
||||
|
||||
if(replace) {
|
||||
window.history.replaceState(undefined, null, url);
|
||||
} else {
|
||||
window.history.pushState(undefined, null, url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clickToReq(ev) {
|
||||
// confirm that this is an <a> that meets our requirements
|
||||
if (
|
||||
ev.defaultPrevented ||
|
||||
ev.button !== 0 ||
|
||||
ev.metaKey ||
|
||||
ev.altKey ||
|
||||
ev.ctrlKey ||
|
||||
ev.shiftKey
|
||||
)
|
||||
return;
|
||||
|
||||
/** @type HTMLAnchorElement | undefined;*/
|
||||
const a = ev
|
||||
.composedPath()
|
||||
.find(el => el instanceof Node && el.nodeName.toUpperCase() === "A");
|
||||
|
||||
if (!a) return;
|
||||
|
||||
const svg = a.namespaceURI === "http://www.w3.org/2000/svg";
|
||||
const href = svg ? a.href.baseVal : a.href;
|
||||
const target = svg ? a.target.baseVal : a.target;
|
||||
if (target || (!href && !a.hasAttribute("state"))) return;
|
||||
|
||||
const rel = (a.getAttribute("rel") || "").split(/\s+/);
|
||||
if (a.hasAttribute("download") || (rel?.includes("external"))) return;
|
||||
|
||||
const url = svg ? new URL(href, document.baseURI) : new URL(href);
|
||||
if (
|
||||
url.origin !== window.location.origin // ||
|
||||
// TODO base
|
||||
//(basePath && url.pathname && !url.pathname.toLowerCase().startsWith(basePath.toLowerCase()))
|
||||
)
|
||||
return;
|
||||
|
||||
return new Request(url);
|
||||
}
|
||||
|
||||
function submitToReq(ev) {
|
||||
event.preventDefault();
|
||||
|
||||
const target = ev.target;
|
||||
/** @type HTMLFormElement */
|
||||
let form;
|
||||
if(target instanceof HTMLFormElement) {
|
||||
form = target;
|
||||
} else {
|
||||
if(!target.form) {
|
||||
return;
|
||||
}
|
||||
form = target.form;
|
||||
}
|
||||
|
||||
const method = form.method.toUpperCase();
|
||||
if(method !== "GET" && method !== "POST") {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = new URL(form.action);
|
||||
let path = url.pathname;
|
||||
const requestInit = {};
|
||||
const data = new FormData(form);
|
||||
|
||||
const params = new URLSearchParams();
|
||||
for (const [key, value] of data.entries()) {
|
||||
params.append(key, value);
|
||||
}
|
||||
|
||||
requestInit.headers = {
|
||||
Accept: "text/html"
|
||||
};
|
||||
if(method === "GET") {
|
||||
path += `?${params.toString()}`;
|
||||
}
|
||||
else {
|
||||
requestInit.method = "POST";
|
||||
requestInit.body = params;
|
||||
}
|
||||
|
||||
return new Request(
|
||||
path,
|
||||
requestInit
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function diffPages(htmlString) {
|
||||
// Use DOMParser to parse the HTML string
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(htmlString, 'text/html');
|
||||
|
||||
diffRange(document, document, doc, doc);
|
||||
}
|
||||
|
||||
function diffRange(oldDocument, oldRoot, newDocument, newRoot, oldEnd, newEnd) {
|
||||
const oldDocWalker = oldDocument.createTreeWalker(oldRoot);
|
||||
const newDocWalker = newDocument.createTreeWalker(newRoot);
|
||||
let oldNode = oldDocWalker.currentNode;
|
||||
let newNode = newDocWalker.currentNode;
|
||||
|
||||
while (oldDocWalker.nextNode() && newDocWalker.nextNode()) {
|
||||
oldNode = oldDocWalker.currentNode;
|
||||
newNode = newDocWalker.currentNode;
|
||||
|
||||
if (oldNode == oldEnd || newNode == newEnd) {
|
||||
break;
|
||||
}
|
||||
|
||||
// if the nodes are different, we need to replace the old with the new
|
||||
// because of the typed view tree, this should never actually happen
|
||||
if (oldNode.nodeType !== newNode.nodeType) {
|
||||
oldNode.replaceWith(newNode);
|
||||
}
|
||||
// if it's a text node, just update the text with the new text
|
||||
else if (oldNode.nodeType === Node.TEXT_NODE) {
|
||||
oldNode.textContent = newNode.textContent;
|
||||
}
|
||||
// if it's an element, replace if it's a different tag, or update attributes
|
||||
else if (oldNode.nodeType === Node.ELEMENT_NODE) {
|
||||
diffElement(oldNode, newNode);
|
||||
}
|
||||
// we use comment "branch marker" nodes to distinguish between different branches in the statically-typed view tree
|
||||
// if one of these marker is hit, then there are two options
|
||||
// 1) it's the same branch, and we just keep walking until the end
|
||||
// 2) it's a different branch, in which case the old can be replaced with the new wholesale
|
||||
else if (oldNode.nodeType === Node.COMMENT_NODE) {
|
||||
const oldText = oldNode.textContent;
|
||||
const newText = newNode.textContent;
|
||||
if(oldText.startsWith("bo-for")) {
|
||||
replaceFor(oldDocument, oldDocWalker, newDocument, newDocWalker, oldNode, newNode);
|
||||
}
|
||||
else if (oldText.startsWith("bo-item")) {
|
||||
// skip, this means we're diffing a new item within a For
|
||||
}
|
||||
else if(oldText.startsWith("bo") && newText !== oldText) {
|
||||
replaceBranch(oldDocWalker, newDocWalker, oldNode, newNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function replaceFor(oldDocument, oldDocWalker, newDocument, newDocWalker, oldNode, newNode) {
|
||||
oldDocWalker.nextNode();
|
||||
newDocWalker.nextNode();
|
||||
const oldRange = new Range();
|
||||
const newRange = new Range();
|
||||
let oldBranches = 1;
|
||||
let newBranches = 1;
|
||||
|
||||
const oldKeys = {};
|
||||
const newKeys = {};
|
||||
|
||||
while(oldBranches > 0) {
|
||||
const c = oldDocWalker.currentNode;
|
||||
if(c.nodeType === Node.COMMENT_NODE) {
|
||||
const t = c.textContent;
|
||||
if(t.startsWith("bo-for")) {
|
||||
oldBranches += 1;
|
||||
} else if(t.startsWith("bc-for")) {
|
||||
|
||||
oldBranches -= 1;
|
||||
} else if (t.startsWith("bo-item")) {
|
||||
const k = t.replace("bo-item-", "");
|
||||
oldKeys[k] = { open: c, close: null };
|
||||
} else if (t.startsWith("bc-item")) {
|
||||
const k = t.replace("bc-item-", "");
|
||||
oldKeys[k].close = c;
|
||||
}
|
||||
}
|
||||
oldDocWalker.nextNode();
|
||||
}
|
||||
while(newBranches > 0) {
|
||||
const c = newDocWalker.currentNode;
|
||||
if(c.nodeType === Node.COMMENT_NODE) {
|
||||
const t = c.textContent;
|
||||
if(t.startsWith("bo-for")) {
|
||||
newBranches += 1;
|
||||
} else if(t.startsWith("bc-for")) {
|
||||
|
||||
newBranches -= 1;
|
||||
} else if (t.startsWith("bo-item")) {
|
||||
const k = t.replace("bo-item-", "");
|
||||
newKeys[k] = { open: c, close: null };
|
||||
} else if (t.startsWith("bc-item")) {
|
||||
const k = t.replace("bc-item-", "");
|
||||
newKeys[k].close = c;
|
||||
}
|
||||
}
|
||||
newDocWalker.nextNode();
|
||||
}
|
||||
|
||||
for(const key in oldKeys) {
|
||||
if(newKeys[key]) {
|
||||
const oldOne = oldKeys[key];
|
||||
const newOne = newKeys[key];
|
||||
const oldRange = new Range();
|
||||
const newRange = new Range();
|
||||
|
||||
// then replace the item in the *new* list with the *old* DOM elements
|
||||
oldRange.setStartAfter(oldOne.open);
|
||||
oldRange.setEndBefore(oldOne.close);
|
||||
newRange.setStartAfter(newOne.open);
|
||||
newRange.setEndBefore(newOne.close);
|
||||
const oldContents = oldRange.extractContents();
|
||||
const newContents = newRange.extractContents();
|
||||
|
||||
// patch the *old* DOM elements with the new ones
|
||||
diffRange(oldDocument, oldContents, newDocument, newContents, oldOne.close, newOne.close);
|
||||
|
||||
// then insert the old DOM elements into the new tree
|
||||
// this means you'll end up with any new attributes or content from the server,
|
||||
// but with any old DOM state (because they are the old elements)
|
||||
newRange.insertNode(oldContents);
|
||||
newOne.open.replaceWith(oldOne.open);
|
||||
newOne.close.replaceWith(oldOne.close);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
oldRange.setStartAfter(oldNode);
|
||||
oldRange.setEndBefore(oldDocWalker.currentNode);
|
||||
newRange.setStartAfter(newNode);
|
||||
newRange.setEndAfter(newDocWalker.currentNode);
|
||||
const newContents = newRange.extractContents();
|
||||
oldRange.deleteContents();
|
||||
oldRange.insertNode(newContents);
|
||||
oldNode.replaceWith(newNode);
|
||||
oldDocWalker.currentNode.replaceWith(newDocWalker.currentNode);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function replaceBranch(oldDocWalker, newDocWalker, oldNode, newNode) {
|
||||
oldDocWalker.nextNode();
|
||||
newDocWalker.nextNode();
|
||||
const oldRange = new Range();
|
||||
const newRange = new Range();
|
||||
let oldBranches = 1;
|
||||
let newBranches = 1;
|
||||
while(oldBranches > 0) {
|
||||
if(oldDocWalker.nextNode()) {
|
||||
if(oldDocWalker.currentNode.nodeType === Node.COMMENT_NODE) {
|
||||
if(oldDocWalker.currentNode.textContent.startsWith("bo")) {
|
||||
oldBranches += 1;
|
||||
} else if(oldDocWalker.currentNode.textContent.startsWith("bc")) {
|
||||
|
||||
oldBranches -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
while(newBranches > 0) {
|
||||
if(newDocWalker.nextNode()) {
|
||||
if(newDocWalker.currentNode.nodeType === Node.COMMENT_NODE) {
|
||||
if(newDocWalker.currentNode.textContent.startsWith("bo")) {
|
||||
newBranches += 1;
|
||||
} else if(newDocWalker.currentNode.textContent.startsWith("bc")) {
|
||||
|
||||
newBranches -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
oldRange.setStartAfter(oldNode);
|
||||
oldRange.setEndBefore(oldDocWalker.currentNode);
|
||||
newRange.setStartAfter(newNode);
|
||||
newRange.setEndAfter(newDocWalker.currentNode);
|
||||
const newContents = newRange.extractContents();
|
||||
oldRange.deleteContents();
|
||||
oldRange.insertNode(newContents);
|
||||
oldNode.replaceWith(newNode);
|
||||
oldDocWalker.currentNode.replaceWith(newDocWalker.currentNode);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function diffElement(oldNode, newNode) {
|
||||
/** @type Element */
|
||||
const oldEl = oldNode;
|
||||
/** @type Element */
|
||||
const newEl = newNode;
|
||||
if (oldEl.tagName !== newEl.tagName) {
|
||||
oldEl.replaceWith(newEl);
|
||||
|
||||
}
|
||||
else {
|
||||
for(const attr of newEl.attributes) {
|
||||
oldEl.setAttribute(attr.name, attr.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(const island of document.querySelectorAll("leptos-island")) {
|
||||
island.$$hydrated = true;
|
||||
}
|
||||
@@ -50,6 +50,10 @@ pub fn HydrationScripts(
|
||||
/// Should be `true` to hydrate in `islands` mode.
|
||||
#[prop(optional)]
|
||||
islands: bool,
|
||||
/// Should be `true` to add the “islands router,” which enables limited client-side routing
|
||||
/// when running in islands mode.
|
||||
#[prop(optional)]
|
||||
islands_router: bool,
|
||||
/// A base url, not including a trailing slash
|
||||
#[prop(optional, into)]
|
||||
root: Option<String>,
|
||||
@@ -98,18 +102,36 @@ pub fn HydrationScripts(
|
||||
include_str!("./hydration_script.js")
|
||||
};
|
||||
|
||||
let islands_router = islands_router
|
||||
.then_some(include_str!("./islands_routing.js"))
|
||||
.unwrap_or_default();
|
||||
|
||||
let root = root.unwrap_or_default();
|
||||
view! {
|
||||
<link rel="modulepreload" href=format!("{root}/{pkg_path}/{js_file_name}.js") nonce=nonce.clone()/>
|
||||
<link
|
||||
rel="preload"
|
||||
href=format!("{root}/{pkg_path}/{wasm_file_name}.wasm")
|
||||
r#as="fetch"
|
||||
r#type="application/wasm"
|
||||
crossorigin=nonce.clone().unwrap_or_default()
|
||||
/>
|
||||
<script type="module" nonce=nonce>
|
||||
{format!("{script}({root:?}, {pkg_path:?}, {js_file_name:?}, {wasm_file_name:?})")}
|
||||
</script>
|
||||
}
|
||||
use_context::<IslandsRouterNavigation>().is_none().then(|| {
|
||||
view! {
|
||||
<link rel="modulepreload" href=format!("{root}/{pkg_path}/{js_file_name}.js") nonce=nonce.clone()/>
|
||||
<link
|
||||
rel="preload"
|
||||
href=format!("{root}/{pkg_path}/{wasm_file_name}.wasm")
|
||||
r#as="fetch"
|
||||
r#type="application/wasm"
|
||||
crossorigin=nonce.clone().unwrap_or_default()
|
||||
/>
|
||||
<script type="module" nonce=nonce>
|
||||
{format!("{script}({root:?}, {pkg_path:?}, {js_file_name:?}, {wasm_file_name:?});{islands_router}")}
|
||||
</script>
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// If this is provided via context, it means that you are using the islands router and
|
||||
/// this is a subsequent navigation, made from the client.
|
||||
///
|
||||
/// This should be provided automatically by a server integration if it detects that the
|
||||
/// header `Islands-Router` is present in the request.
|
||||
///
|
||||
/// This is used to determine how much of the hydration script to include in the page.
|
||||
/// If it is present, then the contents of the `<HydrationScripts>` component will not be
|
||||
/// included, as they only need to be sent to the client once.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct IslandsRouterNavigation;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::borrow::Cow;
|
||||
use tachys::{
|
||||
html::attribute::Attribute,
|
||||
html::attribute::{any_attribute::AnyAttribute, Attribute},
|
||||
hydration::Cursor,
|
||||
ssr::StreamBuilder,
|
||||
view::{
|
||||
@@ -87,6 +87,7 @@ impl<T: Render> Render for View<T> {
|
||||
|
||||
impl<T: RenderHtml> RenderHtml for View<T> {
|
||||
type AsyncOutput = T::AsyncOutput;
|
||||
type Owned = View<T::Owned>;
|
||||
|
||||
const MIN_LENGTH: usize = <T as RenderHtml>::MIN_LENGTH;
|
||||
|
||||
@@ -104,6 +105,7 @@ impl<T: RenderHtml> RenderHtml for View<T> {
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) {
|
||||
#[cfg(debug_assertions)]
|
||||
let vm = self.view_marker.to_owned();
|
||||
@@ -112,8 +114,13 @@ impl<T: RenderHtml> RenderHtml for View<T> {
|
||||
buf.push_str(&format!("<!--hot-reload|{vm}|open-->"));
|
||||
}
|
||||
|
||||
self.inner
|
||||
.to_html_with_buf(buf, position, escape, mark_branches);
|
||||
self.inner.to_html_with_buf(
|
||||
buf,
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
if let Some(vm) = vm.as_ref() {
|
||||
@@ -127,6 +134,7 @@ impl<T: RenderHtml> RenderHtml for View<T> {
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
@@ -142,6 +150,7 @@ impl<T: RenderHtml> RenderHtml for View<T> {
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -157,6 +166,14 @@ impl<T: RenderHtml> RenderHtml for View<T> {
|
||||
) -> Self::State {
|
||||
self.inner.hydrate::<FROM_SERVER>(cursor, position)
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
View {
|
||||
inner: self.inner.into_owned(),
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker: self.view_marker,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ToTemplate> ToTemplate for View<T> {
|
||||
|
||||
@@ -172,12 +172,10 @@ pub mod prelude {
|
||||
actions::*, computed::*, effect::*, graph::untrack, owner::*,
|
||||
signal::*, wrappers::read::*,
|
||||
};
|
||||
pub use server_fn::{self, ServerFnError};
|
||||
pub use server_fn::{self, error::ServerFnError};
|
||||
pub use tachys::{
|
||||
reactive_graph::{bind::BindAttribute, node_ref::*, Suspend},
|
||||
view::{
|
||||
any_view::AnyView, fragment::Fragment, template::ViewTemplate,
|
||||
},
|
||||
view::{fragment::Fragment, template::ViewTemplate},
|
||||
};
|
||||
}
|
||||
pub use export_types::*;
|
||||
|
||||
@@ -19,7 +19,7 @@ use slotmap::{DefaultKey, SlotMap};
|
||||
use std::sync::Arc;
|
||||
use tachys::{
|
||||
either::Either,
|
||||
html::attribute::Attribute,
|
||||
html::attribute::{any_attribute::AnyAttribute, Attribute},
|
||||
hydration::Cursor,
|
||||
reactive_graph::{OwnedView, OwnedViewState},
|
||||
ssr::StreamBuilder,
|
||||
@@ -247,6 +247,7 @@ where
|
||||
// i.e., if this is the child of another Suspense during SSR, don't wait for it: it will handle
|
||||
// itself
|
||||
type AsyncOutput = Self;
|
||||
type Owned = Self;
|
||||
|
||||
const MIN_LENGTH: usize = Chil::MIN_LENGTH;
|
||||
|
||||
@@ -262,9 +263,15 @@ where
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) {
|
||||
self.fallback
|
||||
.to_html_with_buf(buf, position, escape, mark_branches);
|
||||
self.fallback.to_html_with_buf(
|
||||
buf,
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
}
|
||||
|
||||
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
|
||||
@@ -273,6 +280,7 @@ where
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
@@ -303,11 +311,13 @@ where
|
||||
let eff = reactive_graph::effect::Effect::new_isomorphic({
|
||||
move |_| {
|
||||
tasks.track();
|
||||
if tasks.read().is_empty() {
|
||||
if let Some(tx) = tasks_tx.take() {
|
||||
// If the receiver has dropped, it means the ScopedFuture has already
|
||||
// dropped, so it doesn't matter if we manage to send this.
|
||||
_ = tx.send(());
|
||||
if let Some(tasks) = tasks.try_read() {
|
||||
if tasks.is_empty() {
|
||||
if let Some(tx) = tasks_tx.take() {
|
||||
// If the receiver has dropped, it means the ScopedFuture has already
|
||||
// dropped, so it doesn't matter if we manage to send this.
|
||||
_ = tx.send(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -369,6 +379,7 @@ where
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
}
|
||||
Some(None) => {
|
||||
@@ -378,6 +389,7 @@ where
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
}
|
||||
None => {
|
||||
@@ -391,12 +403,14 @@ where
|
||||
self.fallback,
|
||||
&mut fallback_position,
|
||||
mark_branches,
|
||||
extra_attrs.clone(),
|
||||
);
|
||||
buf.push_async_out_of_order_with_nonce(
|
||||
fut,
|
||||
position,
|
||||
mark_branches,
|
||||
nonce_or_not(),
|
||||
extra_attrs,
|
||||
);
|
||||
} else {
|
||||
buf.push_async({
|
||||
@@ -412,6 +426,7 @@ where
|
||||
&mut position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
builder.finish().take_chunks()
|
||||
}
|
||||
@@ -461,6 +476,10 @@ where
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper that prevents [`Suspense`] from waiting for any resource reads that happen inside
|
||||
@@ -513,6 +532,7 @@ where
|
||||
T: RenderHtml + 'static,
|
||||
{
|
||||
type AsyncOutput = Self;
|
||||
type Owned = Self;
|
||||
|
||||
const MIN_LENGTH: usize = T::MIN_LENGTH;
|
||||
|
||||
@@ -528,8 +548,15 @@ where
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) {
|
||||
(self.0)().to_html_with_buf(buf, position, escape, mark_branches);
|
||||
(self.0)().to_html_with_buf(
|
||||
buf,
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
}
|
||||
|
||||
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
|
||||
@@ -538,6 +565,7 @@ where
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
@@ -546,6 +574,7 @@ where
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -556,4 +585,8 @@ where
|
||||
) -> Self::State {
|
||||
(self.0)().hydrate::<FROM_SERVER>(cursor, position)
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,9 +76,9 @@ impl Default for TextProp {
|
||||
}
|
||||
|
||||
impl IntoAttributeValue for TextProp {
|
||||
type Output = Oco<'static, str>;
|
||||
type Output = Arc<dyn Fn() -> Oco<'static, str> + Send + Sync>;
|
||||
|
||||
fn into_attribute_value(self) -> Self::Output {
|
||||
self.get()
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
config = { version = "0.14.1", default-features = false, features = [
|
||||
config = { version = "0.15.8", default-features = false, features = [
|
||||
"toml",
|
||||
"convert-case",
|
||||
] }
|
||||
@@ -20,7 +20,7 @@ thiserror = "2.0"
|
||||
typed-builder = "0.20.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.41", features = ["rt", "macros"] }
|
||||
tokio = { version = "1.43", features = ["rt", "macros"] }
|
||||
tempfile = "3.14"
|
||||
temp-env = { version = "0.3.6", features = ["async_closure"] }
|
||||
|
||||
@@ -28,4 +28,4 @@ temp-env = { version = "0.3.6", features = ["async_closure"] }
|
||||
rustdoc-args = ["--generate-link-to-definition"]
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }
|
||||
|
||||
@@ -12,8 +12,9 @@ use typed_builder::TypedBuilder;
|
||||
|
||||
/// A Struct to allow us to parse LeptosOptions from the file. Not really needed, most interactions should
|
||||
/// occur with LeptosOptions
|
||||
#[derive(Clone, Debug, serde::Deserialize, Default)]
|
||||
#[derive(Clone, Debug, serde::Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[non_exhaustive]
|
||||
pub struct ConfFile {
|
||||
pub leptos_options: LeptosOptions,
|
||||
}
|
||||
@@ -24,9 +25,14 @@ pub struct ConfFile {
|
||||
/// It shares keys with cargo-leptos, to allow for easy interoperability
|
||||
#[derive(TypedBuilder, Debug, Clone, serde::Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[non_exhaustive]
|
||||
pub struct LeptosOptions {
|
||||
/// The name of the WASM and JS files generated by wasm-bindgen. Defaults to the crate name with underscores instead of dashes
|
||||
#[builder(setter(into), default=default_output_name())]
|
||||
/// The name of the WASM and JS files generated by wasm-bindgen.
|
||||
///
|
||||
/// This should match the name that will be output when building your application.
|
||||
///
|
||||
/// You can easily set this using `env!("CARGO_CRATE_NAME")`.
|
||||
#[builder(setter(into))]
|
||||
pub output_name: Arc<str>,
|
||||
/// The path of the all the files generated by cargo-leptos. This defaults to '.' for convenience when integrating with other
|
||||
/// tools.
|
||||
@@ -78,6 +84,40 @@ pub struct LeptosOptions {
|
||||
#[builder(default = default_hash_files())]
|
||||
#[serde(default = "default_hash_files")]
|
||||
pub hash_files: bool,
|
||||
/// The default prefix to use for server functions when generating API routes. Can be
|
||||
/// overridden for individual functions using `#[server(prefix = "...")]` as usual.
|
||||
///
|
||||
/// This is useful to override the default prefix (`/api`) for all server functions without
|
||||
/// needing to manually specify via `#[server(prefix = "...")]` on every server function.
|
||||
#[builder(default, setter(strip_option))]
|
||||
#[serde(default)]
|
||||
pub server_fn_prefix: Option<String>,
|
||||
/// Whether to disable appending the server functions' hashes to the end of their API names.
|
||||
///
|
||||
/// This is useful when an app's client side needs a stable server API. For example, shipping
|
||||
/// the CSR WASM binary in a Tauri app. Tauri app releases are dependent on each platform's
|
||||
/// distribution method (e.g., the Apple App Store or the Google Play Store), which typically
|
||||
/// are much slower than the frequency at which a website can be updated. In addition, it's
|
||||
/// common for users to not have the latest app version installed. In these cases, the CSR WASM
|
||||
/// app would need to be able to continue calling the backend server function API, so the API
|
||||
/// path needs to be consistent and not have a hash appended.
|
||||
///
|
||||
/// Note that the hash suffixes is intended as a way to ensure duplicate API routes are created.
|
||||
/// Without the hash, server functions will need to have unique names to avoid creating
|
||||
/// duplicate routes. Axum will throw an error if a duplicate route is added to the router, but
|
||||
/// Actix will not.
|
||||
#[builder(default)]
|
||||
#[serde(default)]
|
||||
pub disable_server_fn_hash: bool,
|
||||
/// Include the module path of the server function in the API route. This is an alternative
|
||||
/// strategy to prevent duplicate server function API routes (the default strategy is to add
|
||||
/// a hash to the end of the route). Each element of the module path will be separated by a `/`.
|
||||
/// For example, a server function with a fully qualified name of `parent::child::server_fn`
|
||||
/// would have an API route of `/api/parent/child/server_fn` (possibly with a
|
||||
/// different prefix and a hash suffix depending on the values of the other server fn configs).
|
||||
#[builder(default)]
|
||||
#[serde(default)]
|
||||
pub server_fn_mod_path: bool,
|
||||
}
|
||||
|
||||
impl LeptosOptions {
|
||||
@@ -120,20 +160,14 @@ impl LeptosOptions {
|
||||
hash_file: env_w_default("LEPTOS_HASH_FILE_NAME", "hash.txt")?
|
||||
.into(),
|
||||
hash_files: env_w_default("LEPTOS_HASH_FILES", "false")?.parse()?,
|
||||
server_fn_prefix: env_wo_default("SERVER_FN_PREFIX")?,
|
||||
disable_server_fn_hash: env_wo_default("DISABLE_SERVER_FN_HASH")?
|
||||
.is_some(),
|
||||
server_fn_mod_path: env_wo_default("SERVER_FN_MOD_PATH")?.is_some(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LeptosOptions {
|
||||
fn default() -> Self {
|
||||
LeptosOptions::builder().build()
|
||||
}
|
||||
}
|
||||
|
||||
fn default_output_name() -> Arc<str> {
|
||||
env!("CARGO_CRATE_NAME").replace('-', "_").into()
|
||||
}
|
||||
|
||||
fn default_site_root() -> Arc<str> {
|
||||
".".into()
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ or_poisoned = { workspace = true }
|
||||
js-sys = "0.3.74"
|
||||
send_wrapper = "0.6.0"
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
wasm-bindgen = "0.2.97"
|
||||
wasm-bindgen = { workspace = true }
|
||||
serde_json = { version = "1.0", optional = true }
|
||||
serde = { version = "1.0", optional = true }
|
||||
|
||||
@@ -39,4 +39,4 @@ rustdoc-args = ["--generate-link-to-definition"]
|
||||
denylist = ["tracing"]
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }
|
||||
|
||||
@@ -16,7 +16,7 @@ proc-macro = true
|
||||
attribute-derive = { version = "0.10.3", features = ["syn-full"] }
|
||||
cfg-if = "1.0"
|
||||
html-escape = "0.2.13"
|
||||
itertools = "0.13.0"
|
||||
itertools = { workspace = true }
|
||||
prettyplease = "0.2.25"
|
||||
proc-macro-error2 = { version = "2.0", default-features = false }
|
||||
proc-macro2 = "1.0"
|
||||
@@ -25,7 +25,7 @@ syn = { version = "2.0", features = ["full"] }
|
||||
rstml = "0.12.0"
|
||||
leptos_hot_reload = { workspace = true }
|
||||
server_fn_macro = { workspace = true }
|
||||
convert_case = "0.6.0"
|
||||
convert_case = "0.7"
|
||||
uuid = { version = "1.11", features = ["v4"] }
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
|
||||
@@ -51,41 +51,42 @@ trace-component-props = []
|
||||
actix = ["server_fn_macro/actix"]
|
||||
axum = ["server_fn_macro/axum"]
|
||||
generic = ["server_fn_macro/generic"]
|
||||
# Having an erasure feature rather than normal --cfg erase_components for the proc macro crate is a workaround for this rust issue:
|
||||
# https://github.com/rust-lang/cargo/issues/4423
|
||||
# TLDR proc macros will ignore RUSTFLAGS when --target is specified on the cargo command.
|
||||
# This works around the issue by the non proc-macro crate which does see RUSTFLAGS enabling the replacement feature on the proc-macro crate, which wouldn't.
|
||||
# This is automatic as long as the leptos crate is depended upon,
|
||||
# downstream usage should never manually enable this feature.
|
||||
__internal_erase_components = []
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["nightly", "tracing", "trace-component-props"]
|
||||
skip_feature_sets = [
|
||||
[
|
||||
"csr",
|
||||
"hydrate",
|
||||
],
|
||||
[
|
||||
"hydrate",
|
||||
"csr",
|
||||
],
|
||||
[
|
||||
"hydrate",
|
||||
"ssr",
|
||||
],
|
||||
[
|
||||
"actix",
|
||||
"axum",
|
||||
],
|
||||
[
|
||||
"actix",
|
||||
"generic",
|
||||
],
|
||||
[
|
||||
"generic",
|
||||
"axum",
|
||||
],
|
||||
[
|
||||
"csr",
|
||||
"hydrate",
|
||||
],
|
||||
[
|
||||
"hydrate",
|
||||
"csr",
|
||||
],
|
||||
[
|
||||
"hydrate",
|
||||
"ssr",
|
||||
],
|
||||
[
|
||||
"actix",
|
||||
"axum",
|
||||
],
|
||||
[
|
||||
"actix",
|
||||
"generic",
|
||||
],
|
||||
[
|
||||
"generic",
|
||||
"axum",
|
||||
],
|
||||
]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--generate-link-to-definition"]
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = [
|
||||
'cfg(leptos_debuginfo)',
|
||||
'cfg(erase_components)',
|
||||
] }
|
||||
|
||||
@@ -11,13 +11,13 @@ dependencies = [
|
||||
[tasks.test-leptos_macro-example]
|
||||
description = "Tests the leptos_macro/example to check if macro handles doc comments correctly"
|
||||
command = "cargo"
|
||||
args = ["+nightly-2024-08-01", "test", "--doc"]
|
||||
args = ["+nightly-2025-03-05", "test", "--doc"]
|
||||
cwd = "example"
|
||||
install_crate = false
|
||||
|
||||
[tasks.doc-leptos_macro-example]
|
||||
description = "Docs the leptos_macro/example to check if macro handles doc comments correctly"
|
||||
command = "cargo"
|
||||
args = ["+nightly-2024-08-01", "doc"]
|
||||
args = ["+nightly-2025-03-05", "doc"]
|
||||
cwd = "example"
|
||||
install_crate = false
|
||||
|
||||
@@ -32,6 +32,8 @@ pub struct Model {
|
||||
impl Parse for Model {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
let mut item = ItemFn::parse(input)?;
|
||||
maybe_modify_return_type(&mut item.sig.output);
|
||||
|
||||
convert_impl_trait_to_generic(&mut item.sig);
|
||||
|
||||
let docs = Docs::new(&item.attrs);
|
||||
@@ -76,6 +78,39 @@ impl Parse for Model {
|
||||
}
|
||||
}
|
||||
|
||||
/// Exists to fix nested routes defined in a separate component in erased mode,
|
||||
/// by replacing the return type with AnyNestedRoute, which is what it'll be, but is required as the return type for compiler inference.
|
||||
fn maybe_modify_return_type(ret: &mut ReturnType) {
|
||||
#[cfg(feature = "__internal_erase_components")]
|
||||
{
|
||||
if let ReturnType::Type(_, ty) = ret {
|
||||
if let Type::ImplTrait(TypeImplTrait { bounds, .. }) = ty.as_ref() {
|
||||
// If one of the bounds is MatchNestedRoutes, we need to replace the return type with AnyNestedRoute:
|
||||
if bounds.iter().any(|bound| {
|
||||
if let syn::TypeParamBound::Trait(trait_bound) = bound {
|
||||
if trait_bound.path.segments.iter().any(
|
||||
|path_segment| {
|
||||
path_segment.ident == "MatchNestedRoutes"
|
||||
},
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}) {
|
||||
*ty = parse_quote!(
|
||||
::leptos_router::any_nested_route::AnyNestedRoute
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "__internal_erase_components"))]
|
||||
{
|
||||
let _ = ret;
|
||||
}
|
||||
}
|
||||
|
||||
// implemented manually because Vec::drain_filter is nightly only
|
||||
// follows std recommended parallel
|
||||
pub fn drain_filter<T>(
|
||||
@@ -296,9 +331,9 @@ impl ToTokens for Model {
|
||||
|
||||
let component = if *is_transparent {
|
||||
body_expr
|
||||
} else if cfg!(erase_components) {
|
||||
} else if cfg!(feature = "__internal_erase_components") {
|
||||
quote! {
|
||||
::leptos::prelude::IntoAny::into_any(
|
||||
::leptos::prelude::IntoMaybeErased::into_maybe_erased(
|
||||
::leptos::reactive::graph::untrack_with_diagnostics(
|
||||
move || {
|
||||
#tracing_guard_expr
|
||||
@@ -613,7 +648,8 @@ impl Parse for DummyModel {
|
||||
drain_filter(&mut attrs, |attr| !attr.path().is_ident("doc"));
|
||||
|
||||
let vis: Visibility = input.parse()?;
|
||||
let sig: Signature = input.parse()?;
|
||||
let mut sig: Signature = input.parse()?;
|
||||
maybe_modify_return_type(&mut sig.output);
|
||||
|
||||
// The body is left untouched, so it will not cause an error
|
||||
// even if the syntax is invalid.
|
||||
|
||||
@@ -281,7 +281,11 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
#[proc_macro]
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
|
||||
pub fn template(tokens: TokenStream) -> TokenStream {
|
||||
view_macro_impl(tokens, true)
|
||||
if cfg!(feature = "__internal_erase_components") {
|
||||
view(tokens)
|
||||
} else {
|
||||
view_macro_impl(tokens, true)
|
||||
}
|
||||
}
|
||||
|
||||
fn view_macro_impl(tokens: TokenStream, template: bool) -> TokenStream {
|
||||
@@ -923,7 +927,7 @@ pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
args.into(),
|
||||
s.into(),
|
||||
Some(syn::parse_quote!(::leptos::server_fn)),
|
||||
"/api",
|
||||
option_env!("SERVER_FN_PREFIX").unwrap_or("/api"),
|
||||
None,
|
||||
None,
|
||||
) {
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
use super::{
|
||||
fragment_to_tokens, utils::is_nostrip_optional_and_update_key, TagType,
|
||||
};
|
||||
use crate::view::{attribute_absolute, utils::filter_prefixed_attrs};
|
||||
use crate::view::{
|
||||
attribute_absolute, text_to_tokens, utils::filter_prefixed_attrs,
|
||||
};
|
||||
use proc_macro2::{Ident, TokenStream, TokenTree};
|
||||
use quote::{format_ident, quote, quote_spanned};
|
||||
use rstml::node::{
|
||||
CustomNode, KeyedAttributeValue, NodeAttribute, NodeBlock, NodeElement,
|
||||
NodeName,
|
||||
CustomNode, KeyedAttributeValue, Node, NodeAttribute, NodeBlock,
|
||||
NodeElement, NodeName,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use syn::{spanned::Spanned, Expr, ExprPath, ExprRange, RangeLimits, Stmt};
|
||||
use syn::{
|
||||
spanned::Spanned, Expr, ExprPath, ExprRange, Item, RangeLimits, Stmt,
|
||||
};
|
||||
|
||||
pub(crate) fn component_to_tokens(
|
||||
node: &mut NodeElement<impl CustomNode>,
|
||||
@@ -170,8 +174,14 @@ pub(crate) fn component_to_tokens(
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let spreads = (!(spreads.is_empty())).then(|| {
|
||||
quote! {
|
||||
.add_any_attr((#(#spreads,)*).into_attr())
|
||||
if cfg!(feature = "__internal_erase_components") {
|
||||
quote! {
|
||||
.add_any_attr(vec![#(#spreads.into_any_attr(),)*])
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
.add_any_attr((#(#spreads,)*))
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -191,6 +201,12 @@ pub(crate) fn component_to_tokens(
|
||||
let mut slots = HashMap::new();
|
||||
let children = if node.children.is_empty() {
|
||||
quote! {}
|
||||
} else if let Some(children) = maybe_optimised_component_children(
|
||||
&node.children,
|
||||
&items_to_bind,
|
||||
&items_to_clone,
|
||||
) {
|
||||
children
|
||||
} else {
|
||||
let children = fragment_to_tokens(
|
||||
&mut node.children,
|
||||
@@ -219,10 +235,7 @@ pub(crate) fn component_to_tokens(
|
||||
let bindables =
|
||||
items_to_bind.iter().map(|ident| quote! { #ident, });
|
||||
|
||||
let clonables = items_to_clone.iter().map(|ident| {
|
||||
let ident_ref = quote_spanned!(ident.span()=> &#ident);
|
||||
quote! { let #ident = ::core::clone::Clone::clone(#ident_ref); }
|
||||
});
|
||||
let clonables = items_to_clone_to_tokens(&items_to_clone);
|
||||
|
||||
if bindables.len() > 0 {
|
||||
quote_spanned! {children.span()=>
|
||||
@@ -313,3 +326,111 @@ fn is_attr_let(key: &NodeName) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn items_to_clone_to_tokens<'a>(
|
||||
items_to_clone: &'a [Ident],
|
||||
) -> impl Iterator<Item = TokenStream> + 'a {
|
||||
items_to_clone.iter().map(|ident| {
|
||||
let ident_ref = quote_spanned!(ident.span()=> &#ident);
|
||||
quote! { let #ident = ::core::clone::Clone::clone(#ident_ref); }
|
||||
})
|
||||
}
|
||||
|
||||
/// By default all children are placed in an outer closure || #children.
|
||||
/// This is to work with all the variants of the leptos::children::ToChildren::to_children trait.
|
||||
/// Strings are optimised to be passed without the wrapping closure, providing significant compile time and binary size improvements.
|
||||
pub fn maybe_optimised_component_children(
|
||||
children: &[Node<impl CustomNode>],
|
||||
items_to_bind: &[TokenStream],
|
||||
items_to_clone: &[Ident],
|
||||
) -> Option<TokenStream> {
|
||||
// If there are bindables will have to be in a closure:
|
||||
if !items_to_bind.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Filter out comments:
|
||||
let mut children_iter = children
|
||||
.iter()
|
||||
.filter(|child| !matches!(child, Node::Comment(_)));
|
||||
|
||||
let children = if let Some(child) = children_iter.next() {
|
||||
// If more than one child after filtering out comments, don't think we can optimise:
|
||||
if children_iter.next().is_some() {
|
||||
return None;
|
||||
}
|
||||
match child {
|
||||
Node::Text(text) => text_to_tokens(&text.value),
|
||||
Node::RawText(raw) => {
|
||||
let text = raw.to_string_best();
|
||||
let text = syn::LitStr::new(&text, raw.span());
|
||||
text_to_tokens(&text)
|
||||
}
|
||||
// Specifically allow std macros that produce strings:
|
||||
Node::Block(NodeBlock::ValidBlock(block)) => {
|
||||
fn is_supported(mac: &syn::Macro) -> bool {
|
||||
for string_macro in ["format", "include_str"] {
|
||||
if mac.path.is_ident(string_macro) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
if block.stmts.len() > 1 {
|
||||
return None;
|
||||
} else if let Some(stmt) = block.stmts.first() {
|
||||
match stmt {
|
||||
Stmt::Macro(mac) => {
|
||||
// eprintln!("Macro: {:?}", mac.mac.path);
|
||||
if is_supported(&mac.mac) {
|
||||
quote! { #block }
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Stmt::Item(Item::Macro(mac)) => {
|
||||
// eprintln!("Item Macro: {:?}", mac.mac.path);
|
||||
if is_supported(&mac.mac) {
|
||||
quote! { #block }
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Stmt::Expr(Expr::Macro(mac), _) => {
|
||||
// eprintln!("Expr Macro: {:?}", mac.mac.path);
|
||||
if is_supported(&mac.mac) {
|
||||
quote! { #block }
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
} else {
|
||||
return Some(quote! {});
|
||||
}
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
|
||||
// // Debug check to see how many use this optimisation:
|
||||
// static COUNT: std::sync::atomic::AtomicUsize =
|
||||
// std::sync::atomic::AtomicUsize::new(0);
|
||||
// COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
// eprintln!(
|
||||
// "Optimised children: {}",
|
||||
// COUNT.load(std::sync::atomic::Ordering::Relaxed)
|
||||
// );
|
||||
|
||||
let clonables = items_to_clone_to_tokens(items_to_clone);
|
||||
Some(quote_spanned! {children.span()=>
|
||||
.children({
|
||||
#(#clonables)*
|
||||
|
||||
::leptos::children::ToChildren::to_children(::leptos::children::ChildrenOptContainer(#children))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -428,6 +428,12 @@ fn element_children_to_tokens(
|
||||
{ #child }
|
||||
)
|
||||
})
|
||||
} else if cfg!(feature = "__internal_erase_components") {
|
||||
Some(quote! {
|
||||
.child(
|
||||
leptos::tachys::view::iterators::StaticVec::from(vec![#(#children.into_maybe_erased()),*])
|
||||
)
|
||||
})
|
||||
} else if children.len() > 16 {
|
||||
// implementations of various traits used in routing and rendering are implemented for
|
||||
// tuples of sizes 0, 1, 2, 3, ... N. N varies but is > 16. The traits are also implemented
|
||||
@@ -473,6 +479,10 @@ fn fragment_to_tokens(
|
||||
None
|
||||
} else if children.len() == 1 {
|
||||
children.into_iter().next()
|
||||
} else if cfg!(feature = "__internal_erase_components") {
|
||||
Some(quote! {
|
||||
leptos::tachys::view::iterators::StaticVec::from(vec![#(#children.into_maybe_erased()),*])
|
||||
})
|
||||
} else if children.len() > 16 {
|
||||
// implementations of various traits used in routing and rendering are implemented for
|
||||
// tuples of sizes 0, 1, 2, 3, ... N. N varies but is > 16. The traits are also implemented
|
||||
@@ -757,10 +767,18 @@ pub(crate) fn element_to_tokens(
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(quote! {
|
||||
(#(#attributes,)*)
|
||||
#(.add_any_attr(#additions))*
|
||||
})
|
||||
|
||||
if cfg!(feature = "__internal_erase_components") {
|
||||
Some(quote! {
|
||||
vec![#(#attributes.into_any_attr(),)*]
|
||||
#(.add_any_attr(#additions))*
|
||||
})
|
||||
} else {
|
||||
Some(quote! {
|
||||
(#(#attributes,)*)
|
||||
#(.add_any_attr(#additions))*
|
||||
})
|
||||
}
|
||||
} else {
|
||||
let tag = name.to_string();
|
||||
// collect close_tag name to emit semantic information for IDE.
|
||||
@@ -1157,8 +1175,14 @@ pub(crate) fn two_way_binding_to_tokens(
|
||||
let ident =
|
||||
format_ident!("{}", name.to_case(UpperCamel), span = node.key.span());
|
||||
|
||||
quote! {
|
||||
.bind(::leptos::attr::#ident, #value)
|
||||
if name == "group" {
|
||||
quote! {
|
||||
.bind(leptos::tachys::reactive_graph::bind::#ident, #value)
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
.bind(::leptos::attr::#ident, #value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use super::{convert_to_snake_case, ident_from_tag_name};
|
||||
use super::{
|
||||
component_builder::maybe_optimised_component_children,
|
||||
convert_to_snake_case, ident_from_tag_name,
|
||||
};
|
||||
use crate::view::{fragment_to_tokens, utils::filter_prefixed_attrs, TagType};
|
||||
use proc_macro2::{Ident, TokenStream, TokenTree};
|
||||
use quote::{quote, quote_spanned};
|
||||
@@ -70,7 +73,10 @@ pub(crate) fn slot_to_tokens(
|
||||
}
|
||||
});
|
||||
|
||||
let items_to_bind = filter_prefixed_attrs(attrs.iter(), "let:");
|
||||
let items_to_bind = filter_prefixed_attrs(attrs.iter(), "let:")
|
||||
.into_iter()
|
||||
.map(|ident| quote! { #ident })
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let items_to_clone = filter_prefixed_attrs(attrs.iter(), "clone:");
|
||||
|
||||
@@ -83,7 +89,7 @@ pub(crate) fn slot_to_tokens(
|
||||
let value = attr.value().map(|v| {
|
||||
quote! { #v }
|
||||
})?;
|
||||
Some(quote! { (#name, ::leptos::IntoAttribute::into_attribute(#value)) })
|
||||
Some(quote! { (#name, #value) })
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -96,6 +102,12 @@ pub(crate) fn slot_to_tokens(
|
||||
let mut slots = HashMap::new();
|
||||
let children = if node.children.is_empty() {
|
||||
quote! {}
|
||||
} else if let Some(children) = maybe_optimised_component_children(
|
||||
&node.children,
|
||||
&items_to_bind,
|
||||
&items_to_clone,
|
||||
) {
|
||||
children
|
||||
} else {
|
||||
let children = fragment_to_tokens(
|
||||
&mut node.children,
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub mod tests {
|
||||
|
||||
use leptos::{
|
||||
server,
|
||||
server_fn::{codec, ServerFn, ServerFnError},
|
||||
server_fn::{codec, Http, ServerFn, ServerFnError},
|
||||
};
|
||||
use std::any::TypeId;
|
||||
|
||||
@@ -19,8 +18,8 @@ pub mod tests {
|
||||
"/api/my_server_action"
|
||||
);
|
||||
assert_eq!(
|
||||
TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(),
|
||||
TypeId::of::<codec::PostUrl>()
|
||||
TypeId::of::<<MyServerAction as ServerFn>::Protocol>(),
|
||||
TypeId::of::<Http<codec::PostUrl, codec::Json>>()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,8 +31,8 @@ pub mod tests {
|
||||
}
|
||||
assert_eq!(<FooBar as ServerFn>::PATH, "/foo/bar/my_path");
|
||||
assert_eq!(
|
||||
TypeId::of::<<FooBar as ServerFn>::InputEncoding>(),
|
||||
TypeId::of::<codec::Cbor>()
|
||||
TypeId::of::<<FooBar as ServerFn>::Protocol>(),
|
||||
TypeId::of::<Http<codec::Cbor, codec::Cbor>>()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -45,8 +44,8 @@ pub mod tests {
|
||||
}
|
||||
assert_eq!(<FooBar as ServerFn>::PATH, "/foo/bar/my_path");
|
||||
assert_eq!(
|
||||
TypeId::of::<<FooBar as ServerFn>::InputEncoding>(),
|
||||
TypeId::of::<codec::Cbor>()
|
||||
TypeId::of::<<FooBar as ServerFn>::Protocol>(),
|
||||
TypeId::of::<Http<codec::Cbor, codec::Cbor>>()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -58,8 +57,8 @@ pub mod tests {
|
||||
}
|
||||
assert_eq!(<FooBar as ServerFn>::PATH, "/api/my_path");
|
||||
assert_eq!(
|
||||
TypeId::of::<<FooBar as ServerFn>::InputEncoding>(),
|
||||
TypeId::of::<codec::PostUrl>()
|
||||
TypeId::of::<<FooBar as ServerFn>::Protocol>(),
|
||||
TypeId::of::<Http<codec::PostUrl, codec::Json>>()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -74,8 +73,8 @@ pub mod tests {
|
||||
"/api/my_server_action"
|
||||
);
|
||||
assert_eq!(
|
||||
TypeId::of::<<FooBar as ServerFn>::InputEncoding>(),
|
||||
TypeId::of::<codec::PostUrl>()
|
||||
TypeId::of::<<FooBar as ServerFn>::Protocol>(),
|
||||
TypeId::of::<Http<codec::PostUrl, codec::Json>>()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -91,8 +90,8 @@ pub mod tests {
|
||||
"/foo/bar/my_server_action"
|
||||
);
|
||||
assert_eq!(
|
||||
TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(),
|
||||
TypeId::of::<codec::PostUrl>()
|
||||
TypeId::of::<<MyServerAction as ServerFn>::Protocol>(),
|
||||
TypeId::of::<Http<codec::PostUrl, codec::Json>>()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -108,8 +107,8 @@ pub mod tests {
|
||||
"/api/my_server_action"
|
||||
);
|
||||
assert_eq!(
|
||||
TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(),
|
||||
TypeId::of::<codec::GetUrl>()
|
||||
TypeId::of::<<MyServerAction as ServerFn>::Protocol>(),
|
||||
TypeId::of::<Http<codec::GetUrl, codec::Json>>()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -124,8 +123,8 @@ pub mod tests {
|
||||
"/api/path/to/my/endpoint"
|
||||
);
|
||||
assert_eq!(
|
||||
TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(),
|
||||
TypeId::of::<codec::PostUrl>()
|
||||
TypeId::of::<<MyServerAction as ServerFn>::Protocol>(),
|
||||
TypeId::of::<Http<codec::PostUrl, codec::Json>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#[cfg(not(erase_components))]
|
||||
#[cfg(not(feature = "__internal_erase_components"))]
|
||||
#[test]
|
||||
fn ui() {
|
||||
let t = trybuild::TestCases::new();
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
[package]
|
||||
name = "leptos_server"
|
||||
version = { workspace = true }
|
||||
# TODO revert to { workspace = true } before 0.8.0 release
|
||||
# this is a hack because I missing bumping the hydration_context version number before publishing
|
||||
version = "0.8.0-beta"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
@@ -26,8 +28,8 @@ send_wrapper = "0.6"
|
||||
# serialization formats
|
||||
serde = { version = "1.0" }
|
||||
js-sys = { version = "0.3.74", optional = true }
|
||||
wasm-bindgen = { version = "0.2.97", optional = true }
|
||||
serde_json = { version = "1.0" }
|
||||
wasm-bindgen = { version = "0.2.100", optional = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[features]
|
||||
ssr = []
|
||||
@@ -46,4 +48,4 @@ denylist = ["tracing"]
|
||||
rustdoc-args = ["--generate-link-to-definition"]
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }
|
||||
|
||||
@@ -3,7 +3,7 @@ use reactive_graph::{
|
||||
owner::use_context,
|
||||
traits::DefinedAt,
|
||||
};
|
||||
use server_fn::{error::ServerFnErrorSerde, ServerFn, ServerFnError};
|
||||
use server_fn::{error::FromServerFnError, ServerFn};
|
||||
use std::{ops::Deref, panic::Location, sync::Arc};
|
||||
|
||||
/// An error that can be caused by a server action.
|
||||
@@ -42,7 +42,7 @@ where
|
||||
S: ServerFn + 'static,
|
||||
S::Output: 'static,
|
||||
{
|
||||
inner: ArcAction<S, Result<S::Output, ServerFnError<S::Error>>>,
|
||||
inner: ArcAction<S, Result<S::Output, S::Error>>,
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: &'static Location<'static>,
|
||||
}
|
||||
@@ -52,13 +52,14 @@ where
|
||||
S: ServerFn + Clone + Send + Sync + 'static,
|
||||
S::Output: Send + Sync + 'static,
|
||||
S::Error: Send + Sync + 'static,
|
||||
S::Error: FromServerFnError,
|
||||
{
|
||||
/// Creates a new [`ArcAction`] that will call the server function `S` when dispatched.
|
||||
#[track_caller]
|
||||
pub fn new() -> Self {
|
||||
let err = use_context::<ServerActionError>().and_then(|error| {
|
||||
(error.path() == S::PATH)
|
||||
.then(|| ServerFnError::<S::Error>::de(error.err()))
|
||||
.then(|| S::Error::de(error.err()))
|
||||
.map(Err)
|
||||
});
|
||||
Self {
|
||||
@@ -76,7 +77,7 @@ where
|
||||
S: ServerFn + 'static,
|
||||
S::Output: 'static,
|
||||
{
|
||||
type Target = ArcAction<S, Result<S::Output, ServerFnError<S::Error>>>;
|
||||
type Target = ArcAction<S, Result<S::Output, S::Error>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
@@ -131,7 +132,7 @@ where
|
||||
S: ServerFn + 'static,
|
||||
S::Output: 'static,
|
||||
{
|
||||
inner: Action<S, Result<S::Output, ServerFnError<S::Error>>>,
|
||||
inner: Action<S, Result<S::Output, S::Error>>,
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: &'static Location<'static>,
|
||||
}
|
||||
@@ -146,7 +147,7 @@ where
|
||||
pub fn new() -> Self {
|
||||
let err = use_context::<ServerActionError>().and_then(|error| {
|
||||
(error.path() == S::PATH)
|
||||
.then(|| ServerFnError::<S::Error>::de(error.err()))
|
||||
.then(|| S::Error::de(error.err()))
|
||||
.map(Err)
|
||||
});
|
||||
Self {
|
||||
@@ -182,15 +183,14 @@ where
|
||||
S::Output: Send + Sync + 'static,
|
||||
S::Error: Send + Sync + 'static,
|
||||
{
|
||||
type Target = Action<S, Result<S::Output, ServerFnError<S::Error>>>;
|
||||
type Target = Action<S, Result<S::Output, S::Error>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> From<ServerAction<S>>
|
||||
for Action<S, Result<S::Output, ServerFnError<S::Error>>>
|
||||
impl<S> From<ServerAction<S>> for Action<S, Result<S::Output, S::Error>>
|
||||
where
|
||||
S: ServerFn + 'static,
|
||||
S::Output: 'static,
|
||||
|
||||
@@ -79,7 +79,7 @@ mod view_implementations {
|
||||
use reactive_graph::traits::Read;
|
||||
use std::future::Future;
|
||||
use tachys::{
|
||||
html::attribute::Attribute,
|
||||
html::attribute::{any_attribute::AnyAttribute, Attribute},
|
||||
hydration::Cursor,
|
||||
reactive_graph::{RenderEffectState, Suspend, SuspendState},
|
||||
ssr::StreamBuilder,
|
||||
@@ -135,6 +135,7 @@ mod view_implementations {
|
||||
Ser: Send + 'static,
|
||||
{
|
||||
type AsyncOutput = Option<T>;
|
||||
type Owned = Self;
|
||||
|
||||
const MIN_LENGTH: usize = 0;
|
||||
|
||||
@@ -152,12 +153,14 @@ mod view_implementations {
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) {
|
||||
(move || Suspend::new(async move { self.await })).to_html_with_buf(
|
||||
buf,
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -167,6 +170,7 @@ mod view_implementations {
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
@@ -176,6 +180,7 @@ mod view_implementations {
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -187,5 +192,9 @@ mod view_implementations {
|
||||
(move || Suspend::new(async move { self.await }))
|
||||
.hydrate::<FROM_SERVER>(cursor, position)
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,13 +8,15 @@ use reactive_graph::{
|
||||
ToAnySource, ToAnySubscriber,
|
||||
},
|
||||
owner::use_context,
|
||||
send_wrapper_ext::MaybeSendWrapperOption,
|
||||
signal::{
|
||||
guards::{AsyncPlain, ReadGuard},
|
||||
guards::{AsyncPlain, Mapped, ReadGuard},
|
||||
ArcRwSignal, RwSignal,
|
||||
},
|
||||
traits::{DefinedAt, IsDisposed, ReadUntracked, Track, Update, Write},
|
||||
traits::{
|
||||
DefinedAt, IsDisposed, ReadUntracked, Track, Update, With, Write,
|
||||
},
|
||||
};
|
||||
use send_wrapper::SendWrapper;
|
||||
use std::{
|
||||
future::{pending, Future, IntoFuture},
|
||||
panic::Location,
|
||||
@@ -22,7 +24,7 @@ use std::{
|
||||
|
||||
/// A reference-counted resource that only loads its data locally on the client.
|
||||
pub struct ArcLocalResource<T> {
|
||||
data: ArcAsyncDerived<SendWrapper<T>>,
|
||||
data: ArcAsyncDerived<T>,
|
||||
refetch: ArcRwSignal<usize>,
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: &'static Location<'static>,
|
||||
@@ -69,14 +71,12 @@ impl<T> ArcLocalResource<T> {
|
||||
}
|
||||
}
|
||||
};
|
||||
let fetcher = SendWrapper::new(fetcher);
|
||||
let refetch = ArcRwSignal::new(0);
|
||||
let data = {
|
||||
let refetch = refetch.clone();
|
||||
ArcAsyncDerived::new(move || {
|
||||
ArcAsyncDerived::new_unsync(move || {
|
||||
refetch.track();
|
||||
let fut = fetcher();
|
||||
SendWrapper::new(async move { SendWrapper::new(fut.await) })
|
||||
fetcher()
|
||||
})
|
||||
};
|
||||
Self {
|
||||
@@ -91,6 +91,34 @@ impl<T> ArcLocalResource<T> {
|
||||
pub fn refetch(&self) {
|
||||
*self.refetch.write() += 1;
|
||||
}
|
||||
|
||||
/// Synchronously, reactively reads the current value of the resource and applies the function
|
||||
/// `f` to its value if it is `Some(_)`.
|
||||
#[track_caller]
|
||||
pub fn map<U>(&self, f: impl FnOnce(&T) -> U) -> Option<U>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
self.data.try_with(|n| n.as_ref().map(f))?
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> ArcLocalResource<Result<T, E>>
|
||||
where
|
||||
T: 'static,
|
||||
E: Clone + 'static,
|
||||
{
|
||||
/// Applies the given function when a resource that returns `Result<T, E>`
|
||||
/// has resolved and loaded an `Ok(_)`, rather than requiring nested `.map()`
|
||||
/// calls over the `Option<Result<_, _>>` returned by the resource.
|
||||
///
|
||||
/// This is useful when used with features like server functions, in conjunction
|
||||
/// with `<ErrorBoundary/>` and `<Suspense/>`, when these other components are
|
||||
/// left to handle the `None` and `Err(_)` states.
|
||||
#[track_caller]
|
||||
pub fn and_then<U>(&self, f: impl FnOnce(&T) -> U) -> Option<Result<U, E>> {
|
||||
self.map(|data| data.as_ref().map(f).map_err(|e| e.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoFuture for ArcLocalResource<T>
|
||||
@@ -98,14 +126,9 @@ where
|
||||
T: Clone + 'static,
|
||||
{
|
||||
type Output = T;
|
||||
type IntoFuture = futures::future::Map<
|
||||
AsyncDerivedFuture<SendWrapper<T>>,
|
||||
fn(SendWrapper<T>) -> T,
|
||||
>;
|
||||
type IntoFuture = AsyncDerivedFuture<T>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
use futures::FutureExt;
|
||||
|
||||
if let Some(mut notifier) = use_context::<LocalResourceNotifier>() {
|
||||
notifier.notify();
|
||||
} else if cfg!(feature = "ssr") {
|
||||
@@ -115,7 +138,7 @@ where
|
||||
always pending on the server."
|
||||
);
|
||||
}
|
||||
self.data.into_future().map(|value| (*value).clone())
|
||||
self.data.into_future()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,18 +159,14 @@ impl<T> ReadUntracked for ArcLocalResource<T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
type Value =
|
||||
ReadGuard<Option<SendWrapper<T>>, AsyncPlain<Option<SendWrapper<T>>>>;
|
||||
type Value = ReadGuard<
|
||||
Option<T>,
|
||||
Mapped<AsyncPlain<MaybeSendWrapperOption<T>>, Option<T>>,
|
||||
>;
|
||||
|
||||
fn try_read_untracked(&self) -> Option<Self::Value> {
|
||||
if let Some(mut notifier) = use_context::<LocalResourceNotifier>() {
|
||||
notifier.notify();
|
||||
} else if cfg!(feature = "ssr") {
|
||||
panic!(
|
||||
"Reading from a LocalResource outside Suspense in `ssr` mode \
|
||||
will cause the response to hang, because LocalResources are \
|
||||
always pending on the server."
|
||||
);
|
||||
}
|
||||
self.data.try_read_untracked()
|
||||
}
|
||||
@@ -216,7 +235,7 @@ impl<T> Subscriber for ArcLocalResource<T> {
|
||||
|
||||
/// A resource that only loads its data locally on the client.
|
||||
pub struct LocalResource<T> {
|
||||
data: AsyncDerived<SendWrapper<T>>,
|
||||
data: AsyncDerived<T>,
|
||||
refetch: RwSignal<usize>,
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: &'static Location<'static>,
|
||||
@@ -266,11 +285,9 @@ impl<T> LocalResource<T> {
|
||||
data: if cfg!(feature = "ssr") {
|
||||
AsyncDerived::new_mock(fetcher)
|
||||
} else {
|
||||
let fetcher = SendWrapper::new(fetcher);
|
||||
AsyncDerived::new(move || {
|
||||
AsyncDerived::new_unsync_threadsafe_storage(move || {
|
||||
refetch.track();
|
||||
let fut = fetcher();
|
||||
SendWrapper::new(async move { SendWrapper::new(fut.await) })
|
||||
fetcher()
|
||||
})
|
||||
},
|
||||
refetch,
|
||||
@@ -290,14 +307,9 @@ where
|
||||
T: Clone + 'static,
|
||||
{
|
||||
type Output = T;
|
||||
type IntoFuture = futures::future::Map<
|
||||
AsyncDerivedFuture<SendWrapper<T>>,
|
||||
fn(SendWrapper<T>) -> T,
|
||||
>;
|
||||
type IntoFuture = AsyncDerivedFuture<T>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
use futures::FutureExt;
|
||||
|
||||
if let Some(mut notifier) = use_context::<LocalResourceNotifier>() {
|
||||
notifier.notify();
|
||||
} else if cfg!(feature = "ssr") {
|
||||
@@ -307,7 +319,7 @@ where
|
||||
always pending on the server."
|
||||
);
|
||||
}
|
||||
self.data.into_future().map(|value| (*value).clone())
|
||||
self.data.into_future()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,18 +340,14 @@ impl<T> ReadUntracked for LocalResource<T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
type Value =
|
||||
ReadGuard<Option<SendWrapper<T>>, AsyncPlain<Option<SendWrapper<T>>>>;
|
||||
type Value = ReadGuard<
|
||||
Option<T>,
|
||||
Mapped<AsyncPlain<MaybeSendWrapperOption<T>>, Option<T>>,
|
||||
>;
|
||||
|
||||
fn try_read_untracked(&self) -> Option<Self::Value> {
|
||||
if let Some(mut notifier) = use_context::<LocalResourceNotifier>() {
|
||||
notifier.notify();
|
||||
} else if cfg!(feature = "ssr") {
|
||||
panic!(
|
||||
"Reading from a LocalResource outside Suspense in `ssr` mode \
|
||||
will cause the response to hang, because LocalResources are \
|
||||
always pending on the server."
|
||||
);
|
||||
}
|
||||
self.data.try_read_untracked()
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use reactive_graph::{
|
||||
actions::{ArcMultiAction, MultiAction},
|
||||
traits::DefinedAt,
|
||||
};
|
||||
use server_fn::{ServerFn, ServerFnError};
|
||||
use server_fn::ServerFn;
|
||||
use std::{ops::Deref, panic::Location};
|
||||
|
||||
/// An [`ArcMultiAction`] that can be used to call a server function.
|
||||
@@ -11,7 +11,7 @@ where
|
||||
S: ServerFn + 'static,
|
||||
S::Output: 'static,
|
||||
{
|
||||
inner: ArcMultiAction<S, Result<S::Output, ServerFnError<S::Error>>>,
|
||||
inner: ArcMultiAction<S, Result<S::Output, S::Error>>,
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: &'static Location<'static>,
|
||||
}
|
||||
@@ -40,7 +40,7 @@ where
|
||||
S: ServerFn + 'static,
|
||||
S::Output: 'static,
|
||||
{
|
||||
type Target = ArcMultiAction<S, Result<S::Output, ServerFnError<S::Error>>>;
|
||||
type Target = ArcMultiAction<S, Result<S::Output, S::Error>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
@@ -95,13 +95,13 @@ where
|
||||
S: ServerFn + 'static,
|
||||
S::Output: 'static,
|
||||
{
|
||||
inner: MultiAction<S, Result<S::Output, ServerFnError<S::Error>>>,
|
||||
inner: MultiAction<S, Result<S::Output, S::Error>>,
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: &'static Location<'static>,
|
||||
}
|
||||
|
||||
impl<S> From<ServerMultiAction<S>>
|
||||
for MultiAction<S, Result<S::Output, ServerFnError<S::Error>>>
|
||||
for MultiAction<S, Result<S::Output, S::Error>>
|
||||
where
|
||||
S: ServerFn + 'static,
|
||||
S::Output: 'static,
|
||||
@@ -152,7 +152,7 @@ where
|
||||
S::Output: 'static,
|
||||
S::Error: 'static,
|
||||
{
|
||||
type Target = MultiAction<S, Result<S::Output, ServerFnError<S::Error>>>;
|
||||
type Target = MultiAction<S, Result<S::Output, S::Error>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
|
||||
@@ -168,6 +168,41 @@ where
|
||||
|
||||
data
|
||||
}
|
||||
|
||||
/// Synchronously, reactively reads the current value of the resource and applies the function
|
||||
/// `f` to its value if it is `Some(_)`.
|
||||
#[track_caller]
|
||||
pub fn map<U>(&self, f: impl FnOnce(&T) -> U) -> Option<U>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
self.try_with(|n| n.as_ref().map(f))?
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E, Ser> ArcOnceResource<Result<T, E>, Ser>
|
||||
where
|
||||
Ser: Encoder<Result<T, E>> + Decoder<Result<T, E>>,
|
||||
<Ser as Encoder<Result<T, E>>>::Error: Debug,
|
||||
<Ser as Decoder<Result<T, E>>>::Error: Debug,
|
||||
<<Ser as Decoder<Result<T, E>>>::Encoded as FromEncodedStr>::DecodingError:
|
||||
Debug,
|
||||
<Ser as Encoder<Result<T, E>>>::Encoded: IntoEncodedString,
|
||||
<Ser as Decoder<Result<T, E>>>::Encoded: FromEncodedStr,
|
||||
T: Send + Sync + 'static,
|
||||
E: Send + Sync + Clone + 'static,
|
||||
{
|
||||
/// Applies the given function when a resource that returns `Result<T, E>`
|
||||
/// has resolved and loaded an `Ok(_)`, rather than requiring nested `.map()`
|
||||
/// calls over the `Option<Result<_, _>>` returned by the resource.
|
||||
///
|
||||
/// This is useful when used with features like server functions, in conjunction
|
||||
/// with `<ErrorBoundary/>` and `<Suspense/>`, when these other components are
|
||||
/// left to handle the `None` and `Err(_)` states.
|
||||
#[track_caller]
|
||||
pub fn and_then<U>(&self, f: impl FnOnce(&T) -> U) -> Option<Result<U, E>> {
|
||||
self.map(|data| data.as_ref().map(f).map_err(|e| e.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> ArcOnceResource<T, Ser> {
|
||||
@@ -534,6 +569,37 @@ where
|
||||
defined_at,
|
||||
}
|
||||
}
|
||||
|
||||
/// Synchronously, reactively reads the current value of the resource and applies the function
|
||||
/// `f` to its value if it is `Some(_)`.
|
||||
pub fn map<U>(&self, f: impl FnOnce(&T) -> U) -> Option<U> {
|
||||
self.try_with(|n| n.as_ref().map(|n| Some(f(n))))?.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E, Ser> OnceResource<Result<T, E>, Ser>
|
||||
where
|
||||
Ser: Encoder<Result<T, E>> + Decoder<Result<T, E>>,
|
||||
<Ser as Encoder<Result<T, E>>>::Error: Debug,
|
||||
<Ser as Decoder<Result<T, E>>>::Error: Debug,
|
||||
<<Ser as Decoder<Result<T, E>>>::Encoded as FromEncodedStr>::DecodingError:
|
||||
Debug,
|
||||
<Ser as Encoder<Result<T, E>>>::Encoded: IntoEncodedString,
|
||||
<Ser as Decoder<Result<T, E>>>::Encoded: FromEncodedStr,
|
||||
T: Send + Sync + 'static,
|
||||
E: Send + Sync + Clone + 'static,
|
||||
{
|
||||
/// Applies the given function when a resource that returns `Result<T, E>`
|
||||
/// has resolved and loaded an `Ok(_)`, rather than requiring nested `.map()`
|
||||
/// calls over the `Option<Result<_, _>>` returned by the resource.
|
||||
///
|
||||
/// This is useful when used with features like server functions, in conjunction
|
||||
/// with `<ErrorBoundary/>` and `<Suspense/>`, when these other components are
|
||||
/// left to handle the `None` and `Err(_)` states.
|
||||
#[track_caller]
|
||||
pub fn and_then<U>(&self, f: impl FnOnce(&T) -> U) -> Option<Result<U, E>> {
|
||||
self.map(|data| data.as_ref().map(f).map_err(|e| e.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> OnceResource<T, Ser>
|
||||
|
||||
@@ -215,16 +215,11 @@ where
|
||||
None
|
||||
}
|
||||
Ok(encoded) => {
|
||||
match Ser::decode(encoded.borrow()) {
|
||||
#[allow(unused_variables)]
|
||||
// used in tracing
|
||||
Err(e) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::error!("{e:?}");
|
||||
None
|
||||
}
|
||||
Ok(value) => Some(value),
|
||||
}
|
||||
let decoded = Ser::decode(encoded.borrow());
|
||||
#[cfg(feature = "tracing")]
|
||||
let decoded = decoded
|
||||
.inspect_err(|e| tracing::error!("{e:?}"));
|
||||
decoded.ok()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_meta"
|
||||
version = "0.7.7"
|
||||
version = "0.8.0-beta"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
@@ -15,7 +15,7 @@ or_poisoned = { workspace = true }
|
||||
indexmap = "2.6"
|
||||
send_wrapper = "0.6.0"
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
wasm-bindgen = "0.2.97"
|
||||
wasm-bindgen = { workspace = true }
|
||||
futures = "0.3.31"
|
||||
|
||||
[dependencies.web-sys]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::ServerMetaContext;
|
||||
use leptos::{
|
||||
attr::NextAttribute,
|
||||
attr::{any_attribute::AnyAttribute, NextAttribute},
|
||||
component, html,
|
||||
reactive::owner::use_context,
|
||||
tachys::{
|
||||
@@ -103,6 +103,7 @@ where
|
||||
At: Attribute,
|
||||
{
|
||||
type AsyncOutput = BodyView<At::AsyncOutput>;
|
||||
type Owned = BodyView<At::CloneableOwned>;
|
||||
|
||||
const MIN_LENGTH: usize = At::MIN_LENGTH;
|
||||
|
||||
@@ -122,10 +123,14 @@ where
|
||||
_position: &mut Position,
|
||||
_escape: bool,
|
||||
_mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) {
|
||||
if let Some(meta) = use_context::<ServerMetaContext>() {
|
||||
let mut buf = String::new();
|
||||
_ = html::attributes_to_html(self.attributes, &mut buf);
|
||||
_ = html::attributes_to_html(
|
||||
(self.attributes, extra_attrs),
|
||||
&mut buf,
|
||||
);
|
||||
if !buf.is_empty() {
|
||||
_ = meta.body.send(buf);
|
||||
}
|
||||
@@ -142,6 +147,12 @@ where
|
||||
|
||||
BodyViewState { attributes }
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
BodyView {
|
||||
attributes: self.attributes.into_cloneable_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<At> Mountable for BodyViewState<At>
|
||||
@@ -160,4 +171,11 @@ where
|
||||
fn insert_before_this(&self, _child: &mut dyn Mountable) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn elements(&self) -> Vec<leptos::tachys::renderer::types::Element> {
|
||||
vec![document()
|
||||
.body()
|
||||
.expect("there to be a <body> element")
|
||||
.into()]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::ServerMetaContext;
|
||||
use leptos::{
|
||||
attr::NextAttribute,
|
||||
attr::{any_attribute::AnyAttribute, NextAttribute},
|
||||
component, html,
|
||||
reactive::owner::use_context,
|
||||
tachys::{
|
||||
@@ -103,6 +103,7 @@ where
|
||||
At: Attribute,
|
||||
{
|
||||
type AsyncOutput = HtmlView<At::AsyncOutput>;
|
||||
type Owned = HtmlView<At::CloneableOwned>;
|
||||
|
||||
const MIN_LENGTH: usize = At::MIN_LENGTH;
|
||||
|
||||
@@ -122,10 +123,14 @@ where
|
||||
_position: &mut Position,
|
||||
_escape: bool,
|
||||
_mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) {
|
||||
if let Some(meta) = use_context::<ServerMetaContext>() {
|
||||
let mut buf = String::new();
|
||||
_ = html::attributes_to_html(self.attributes, &mut buf);
|
||||
_ = html::attributes_to_html(
|
||||
(self.attributes, extra_attrs),
|
||||
&mut buf,
|
||||
);
|
||||
if !buf.is_empty() {
|
||||
_ = meta.html.send(buf);
|
||||
}
|
||||
@@ -145,6 +150,12 @@ where
|
||||
|
||||
HtmlViewState { attributes }
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
HtmlView {
|
||||
attributes: self.attributes.into_cloneable_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<At> Mountable for HtmlViewState<At>
|
||||
@@ -165,4 +176,10 @@ where
|
||||
fn insert_before_this(&self, _child: &mut dyn Mountable) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn elements(&self) -> Vec<leptos::tachys::renderer::types::Element> {
|
||||
vec![document()
|
||||
.document_element()
|
||||
.expect("there to be a <html> element")]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
use futures::{Stream, StreamExt};
|
||||
use leptos::{
|
||||
attr::NextAttribute,
|
||||
attr::{any_attribute::AnyAttribute, NextAttribute},
|
||||
component,
|
||||
logging::debug_warn,
|
||||
oco::Oco,
|
||||
@@ -323,37 +323,13 @@ pub(crate) fn register<E, At, Ch>(
|
||||
where
|
||||
HtmlElement<E, At, Ch>: RenderHtml,
|
||||
{
|
||||
#[allow(unused_mut)] // used for `ssr`
|
||||
let mut el = Some(el);
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
if let Some(cx) = use_context::<ServerMetaContext>() {
|
||||
let mut buf = String::new();
|
||||
el.take().unwrap().to_html_with_buf(
|
||||
&mut buf,
|
||||
&mut Position::NextChild,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
_ = cx.elements.send(buf); // fails only if the receiver is already dropped
|
||||
} else {
|
||||
let msg = "tried to use a leptos_meta component without \
|
||||
`ServerMetaContext` provided";
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::warn!("{}", msg);
|
||||
|
||||
#[cfg(not(feature = "tracing"))]
|
||||
eprintln!("{}", msg);
|
||||
}
|
||||
|
||||
RegisteredMetaTag { el }
|
||||
}
|
||||
|
||||
struct RegisteredMetaTag<E, At, Ch> {
|
||||
// this is `None` if we've already taken it out to render to HTML on the server
|
||||
// we don't render it in place in RenderHtml, so it's fine
|
||||
el: Option<HtmlElement<E, At, Ch>>,
|
||||
el: HtmlElement<E, At, Ch>,
|
||||
}
|
||||
|
||||
struct RegisteredMetaTagState<E, At, Ch>
|
||||
@@ -391,12 +367,12 @@ where
|
||||
type State = RegisteredMetaTagState<E, At, Ch>;
|
||||
|
||||
fn build(self) -> Self::State {
|
||||
let state = self.el.unwrap().build();
|
||||
let state = self.el.build();
|
||||
RegisteredMetaTagState { state }
|
||||
}
|
||||
|
||||
fn rebuild(self, state: &mut Self::State) {
|
||||
self.el.unwrap().rebuild(&mut state.state);
|
||||
self.el.rebuild(&mut state.state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -417,7 +393,7 @@ where
|
||||
Self::Output<NewAttr>: RenderHtml,
|
||||
{
|
||||
RegisteredMetaTag {
|
||||
el: self.el.map(|inner| inner.add_any_attr(attr)),
|
||||
el: self.el.add_any_attr(attr),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -429,6 +405,7 @@ where
|
||||
Ch: RenderHtml + Send,
|
||||
{
|
||||
type AsyncOutput = Self;
|
||||
type Owned = RegisteredMetaTag<E, At::CloneableOwned, Ch::Owned>;
|
||||
|
||||
const MIN_LENGTH: usize = 0;
|
||||
|
||||
@@ -446,9 +423,31 @@ where
|
||||
_position: &mut Position,
|
||||
_escape: bool,
|
||||
_mark_branches: bool,
|
||||
_extra_attrs: Vec<AnyAttribute>,
|
||||
) {
|
||||
// meta tags are rendered into the buffer stored into the context
|
||||
// the value has already been taken out, when we're on the server
|
||||
#[cfg(feature = "ssr")]
|
||||
if let Some(cx) = use_context::<ServerMetaContext>() {
|
||||
let mut buf = String::new();
|
||||
self.el.to_html_with_buf(
|
||||
&mut buf,
|
||||
&mut Position::NextChild,
|
||||
false,
|
||||
false,
|
||||
vec![],
|
||||
);
|
||||
_ = cx.elements.send(buf); // fails only if the receiver is already dropped
|
||||
} else {
|
||||
let msg = "tried to use a leptos_meta component without \
|
||||
`ServerMetaContext` provided";
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::warn!("{}", msg);
|
||||
|
||||
#[cfg(not(feature = "tracing"))]
|
||||
eprintln!("{}", msg);
|
||||
}
|
||||
}
|
||||
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
@@ -462,12 +461,18 @@ where
|
||||
MetaContext provided",
|
||||
)
|
||||
.cursor;
|
||||
let state = self.el.unwrap().hydrate::<FROM_SERVER>(
|
||||
let state = self.el.hydrate::<FROM_SERVER>(
|
||||
&cursor,
|
||||
&PositionState::new(Position::NextChild),
|
||||
);
|
||||
RegisteredMetaTagState { state }
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
RegisteredMetaTag {
|
||||
el: self.el.into_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E, At, Ch> Mountable for RegisteredMetaTagState<E, At, Ch>
|
||||
@@ -500,6 +505,10 @@ where
|
||||
// we intended!
|
||||
false
|
||||
}
|
||||
|
||||
fn elements(&self) -> Vec<leptos::tachys::renderer::types::Element> {
|
||||
self.state.elements()
|
||||
}
|
||||
}
|
||||
|
||||
/// During server rendering, inserts the meta tags that have been generated by the other components
|
||||
@@ -541,6 +550,7 @@ impl AddAnyAttr for MetaTagsView {
|
||||
|
||||
impl RenderHtml for MetaTagsView {
|
||||
type AsyncOutput = Self;
|
||||
type Owned = Self;
|
||||
|
||||
const MIN_LENGTH: usize = 0;
|
||||
|
||||
@@ -556,6 +566,7 @@ impl RenderHtml for MetaTagsView {
|
||||
_position: &mut Position,
|
||||
_escape: bool,
|
||||
_mark_branches: bool,
|
||||
_extra_attrs: Vec<AnyAttribute>,
|
||||
) {
|
||||
buf.push_str("<!--HEAD-->");
|
||||
}
|
||||
@@ -566,6 +577,10 @@ impl RenderHtml for MetaTagsView {
|
||||
_position: &PositionState,
|
||||
) -> Self::State {
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait OrDefaultNonce {
|
||||
|
||||
@@ -36,6 +36,10 @@ pub fn Stylesheet(
|
||||
}
|
||||
|
||||
/// Injects an [`HTMLLinkElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement) into the document head that loads a `cargo-leptos`-hashed stylesheet.
|
||||
///
|
||||
/// This should only be used in the application’s server-side `shell` function, as
|
||||
/// [`LeptosOptions`] is not available in the browser. Unlike other `leptos_meta` components, it
|
||||
/// will render the `<link>` it creates exactly where it is called.
|
||||
#[component]
|
||||
pub fn HashedStylesheet(
|
||||
/// Leptos options
|
||||
@@ -74,11 +78,9 @@ pub fn HashedStylesheet(
|
||||
css_file_name.push_str(".css");
|
||||
let pkg_path = &options.site_pkg_dir;
|
||||
let root = root.unwrap_or_default();
|
||||
// TODO additional attributes
|
||||
register(
|
||||
link()
|
||||
.id(id)
|
||||
.rel("stylesheet")
|
||||
.href(format!("{root}/{pkg_path}/{css_file_name}")),
|
||||
)
|
||||
|
||||
link()
|
||||
.id(id)
|
||||
.rel("stylesheet")
|
||||
.href(format!("{root}/{pkg_path}/{css_file_name}"))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{use_head, MetaContext, ServerMetaContext};
|
||||
use leptos::{
|
||||
attr::Attribute,
|
||||
attr::{any_attribute::AnyAttribute, Attribute},
|
||||
component,
|
||||
oco::Oco,
|
||||
reactive::{
|
||||
@@ -234,6 +234,7 @@ impl AddAnyAttr for TitleView {
|
||||
|
||||
impl RenderHtml for TitleView {
|
||||
type AsyncOutput = Self;
|
||||
type Owned = Self;
|
||||
|
||||
const MIN_LENGTH: usize = 0;
|
||||
|
||||
@@ -249,6 +250,7 @@ impl RenderHtml for TitleView {
|
||||
_position: &mut Position,
|
||||
_escape: bool,
|
||||
_mark_branches: bool,
|
||||
_extra_attrs: Vec<AnyAttribute>,
|
||||
) {
|
||||
// meta tags are rendered into the buffer stored into the context
|
||||
// the value has already been taken out, when we're on the server
|
||||
@@ -282,6 +284,10 @@ impl RenderHtml for TitleView {
|
||||
});
|
||||
TitleViewState { effect }
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Mountable for TitleViewState {
|
||||
@@ -299,4 +305,8 @@ impl Mountable for TitleViewState {
|
||||
fn insert_before_this(&self, _child: &mut dyn Mountable) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn elements(&self) -> Vec<leptos::tachys::renderer::types::Element> {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,4 +13,4 @@ serde = "1.0"
|
||||
thiserror = "2.0"
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = "1.0"
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -35,7 +35,7 @@ pub trait OrPoisoned {
|
||||
fn or_poisoned(self) -> Self::Inner;
|
||||
}
|
||||
|
||||
impl<'a, T> OrPoisoned
|
||||
impl<'a, T: ?Sized> OrPoisoned
|
||||
for Result<RwLockReadGuard<'a, T>, PoisonError<RwLockReadGuard<'a, T>>>
|
||||
{
|
||||
type Inner = RwLockReadGuard<'a, T>;
|
||||
@@ -45,7 +45,7 @@ impl<'a, T> OrPoisoned
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> OrPoisoned
|
||||
impl<'a, T: ?Sized> OrPoisoned
|
||||
for Result<RwLockWriteGuard<'a, T>, PoisonError<RwLockWriteGuard<'a, T>>>
|
||||
{
|
||||
type Inner = RwLockWriteGuard<'a, T>;
|
||||
@@ -55,7 +55,7 @@ impl<'a, T> OrPoisoned
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> OrPoisoned for LockResult<MutexGuard<'a, T>> {
|
||||
impl<'a, T: ?Sized> OrPoisoned for LockResult<MutexGuard<'a, T>> {
|
||||
type Inner = MutexGuard<'a, T>;
|
||||
|
||||
fn or_poisoned(self) -> Self::Inner {
|
||||
|
||||
@@ -8,19 +8,19 @@ codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { version = "0.6.13", features = ["csr"] }
|
||||
leptos_meta = { version = "0.6.13", features = ["csr"] }
|
||||
leptos_router = { version = "0.6.13", features = ["csr"] }
|
||||
leptos = { version = "0.7.7", features = ["csr"] }
|
||||
leptos_meta = { version = "0.7.7" }
|
||||
leptos_router = { version = "0.7.7" }
|
||||
console_log = "1.0"
|
||||
log = "0.4.22"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
bevy = "0.14.1"
|
||||
bevy = "0.15.2"
|
||||
crossbeam-channel = "0.5.13"
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen = "0.2.92"
|
||||
wasm-bindgen-test = "0.3.42"
|
||||
web-sys = "0.3.69"
|
||||
wasm-bindgen = "0.2.100"
|
||||
wasm-bindgen-test = "0.3.50"
|
||||
web-sys = "0.3.77"
|
||||
|
||||
[workspace]
|
||||
# The empty workspace here is to keep rust-analyzer satisfied
|
||||
|
||||
@@ -17,7 +17,7 @@ impl DuplexEventsPlugin {
|
||||
let (bevy_sender, client_receiver) = crossbeam_channel::bounded(50);
|
||||
// For sending message from the client to bevy
|
||||
let (client_sender, bevy_receiver) = crossbeam_channel::bounded(50);
|
||||
let instance = DuplexEventsPlugin {
|
||||
DuplexEventsPlugin {
|
||||
client_processor: EventProcessor {
|
||||
sender: client_sender,
|
||||
receiver: client_receiver,
|
||||
@@ -26,8 +26,7 @@ impl DuplexEventsPlugin {
|
||||
sender: bevy_sender,
|
||||
receiver: bevy_receiver,
|
||||
},
|
||||
};
|
||||
instance
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the client event processor
|
||||
|
||||
@@ -23,14 +23,13 @@ impl Scene {
|
||||
/// Create a new instance
|
||||
pub fn new(canvas_id: String) -> Scene {
|
||||
let plugin = DuplexEventsPlugin::new();
|
||||
let instance = Scene {
|
||||
Scene {
|
||||
is_setup: false,
|
||||
canvas_id: canvas_id,
|
||||
canvas_id,
|
||||
evt_plugin: plugin.clone(),
|
||||
shared_state: SharedState::new(),
|
||||
processor: plugin.get_processor(),
|
||||
};
|
||||
instance
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the shared state
|
||||
@@ -47,7 +46,7 @@ impl Scene {
|
||||
|
||||
/// Setup and attach the bevy instance to the html canvas element
|
||||
pub fn setup(&mut self) {
|
||||
if self.is_setup == true {
|
||||
if self.is_setup {
|
||||
return;
|
||||
};
|
||||
App::new()
|
||||
@@ -76,40 +75,37 @@ fn setup_scene(
|
||||
) {
|
||||
let name = resource.0.lock().unwrap().name.clone();
|
||||
// circular base
|
||||
commands.spawn(PbrBundle {
|
||||
mesh: meshes.add(Circle::new(4.0)),
|
||||
material: materials.add(Color::WHITE),
|
||||
transform: Transform::from_rotation(Quat::from_rotation_x(
|
||||
commands.spawn((
|
||||
Mesh3d(meshes.add(Circle::new(4.0))),
|
||||
MeshMaterial3d(materials.add(Color::WHITE)),
|
||||
Transform::from_rotation(Quat::from_rotation_x(
|
||||
-std::f32::consts::FRAC_PI_2,
|
||||
)),
|
||||
..default()
|
||||
});
|
||||
));
|
||||
|
||||
// cube
|
||||
commands.spawn((
|
||||
PbrBundle {
|
||||
mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)),
|
||||
material: materials.add(Color::rgb_u8(124, 144, 255)),
|
||||
transform: Transform::from_xyz(0.0, 0.5, 0.0),
|
||||
..default()
|
||||
},
|
||||
Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))),
|
||||
MeshMaterial3d(materials.add(Color::srgb_u8(124, 144, 255))),
|
||||
Transform::from_xyz(0.0, 0.5, 0.0),
|
||||
Cube,
|
||||
));
|
||||
|
||||
// light
|
||||
commands.spawn(PointLightBundle {
|
||||
point_light: PointLight {
|
||||
commands.spawn((
|
||||
PointLight {
|
||||
shadows_enabled: true,
|
||||
..default()
|
||||
},
|
||||
transform: Transform::from_xyz(4.0, 8.0, 4.0),
|
||||
..default()
|
||||
});
|
||||
Transform::from_xyz(4.0, 8.0, 4.0),
|
||||
));
|
||||
|
||||
// camera
|
||||
commands.spawn(Camera3dBundle {
|
||||
transform: Transform::from_xyz(-2.5, 4.5, 9.0)
|
||||
.looking_at(Vec3::ZERO, Vec3::Y),
|
||||
..default()
|
||||
});
|
||||
commands.spawn(TextBundle::from_section(name, TextStyle::default()));
|
||||
commands.spawn((
|
||||
Camera3d::default(),
|
||||
Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),
|
||||
));
|
||||
commands.spawn((Text::new(name), TextFont::default()));
|
||||
}
|
||||
|
||||
/// Move the Cube on event
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
mod demos;
|
||||
mod routes;
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use routes::RootPage;
|
||||
|
||||
pub fn main() {
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::demos::bevydemo1::eventqueue::events::{
|
||||
ClientInEvents, CounterEvtData,
|
||||
};
|
||||
use crate::demos::bevydemo1::scene::Scene;
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
|
||||
/// 3d view component
|
||||
#[component]
|
||||
@@ -10,18 +10,18 @@ pub fn Demo1() -> impl IntoView {
|
||||
// Setup a Counter
|
||||
let initial_value: i32 = 0;
|
||||
let step: i32 = 1;
|
||||
let (value, set_value) = create_signal(initial_value);
|
||||
let (value, set_value) = signal(initial_value);
|
||||
|
||||
// Setup a bevy 3d scene
|
||||
let scene = Scene::new("#bevy".to_string());
|
||||
let sender = scene.get_processor().sender;
|
||||
let (sender_sig, _set_sender_sig) = create_signal(sender);
|
||||
let (scene_sig, _set_scene_sig) = create_signal(scene);
|
||||
let (sender_sig, _set_sender_sig) = signal(sender);
|
||||
let (scene_sig, _set_scene_sig) = signal(scene);
|
||||
|
||||
// We need to add the 3D view onto the canvas post render.
|
||||
create_effect(move |_| {
|
||||
Effect::new(move |_| {
|
||||
request_animation_frame(move || {
|
||||
scene_sig.get().setup();
|
||||
scene_sig.get_untracked().setup();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
pub mod demo1;
|
||||
use demo1::Demo1;
|
||||
use leptos::*;
|
||||
use leptos_meta::{provide_meta_context, Meta, Stylesheet, Title};
|
||||
use leptos_router::*;
|
||||
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::Meta;
|
||||
use leptos_meta::Title;
|
||||
use leptos_meta::{provide_meta_context, MetaTags, Stylesheet};
|
||||
use leptos_router::components::*;
|
||||
use leptos_router::StaticSegment;
|
||||
#[component]
|
||||
pub fn RootPage() -> impl IntoView {
|
||||
provide_meta_context();
|
||||
@@ -13,11 +15,12 @@ pub fn RootPage() -> impl IntoView {
|
||||
<Meta name="description" content="Leptonic CSR template"/>
|
||||
<Meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<Meta name="theme-color" content="#e66956"/>
|
||||
<Stylesheet href="https://fonts.googleapis.com/css?family=Roboto&display=swap"/>
|
||||
<Title text="Leptos Bevy3D Example"/>
|
||||
<Stylesheet href="https://fonts.googleapis.com/css?family=Roboto&display=swap"/>
|
||||
<MetaTags/>
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="" view=|| view! { <Demo1/> }/>
|
||||
<Routes fallback=move || "Not found.">
|
||||
<Route path=StaticSegment("") view=Demo1 />
|
||||
</Routes>
|
||||
</Router>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "reactive_graph"
|
||||
version = "0.1.7"
|
||||
version = "0.2.0-beta"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -28,7 +28,7 @@ send_wrapper = { version = "0.6.0", features = ["futures"] }
|
||||
web-sys = { version = "0.3.72", features = ["console"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.41", features = ["rt-multi-thread", "macros"] }
|
||||
tokio = { version = "1.43", features = ["rt-multi-thread", "macros"] }
|
||||
tokio-test = { version = "0.4.4" }
|
||||
any_spawner = { workspace = true, features = ["futures-executor", "tokio"] }
|
||||
|
||||
|
||||
@@ -961,11 +961,10 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<I, O, S> Action<I, O, S>
|
||||
impl<I, O> Action<I, O>
|
||||
where
|
||||
I: Send + Sync + 'static,
|
||||
O: Send + Sync + 'static,
|
||||
S: Storage<ArcAction<I, O>>,
|
||||
{
|
||||
/// Creates a new action, which does not require the action itself to be `Send`, but will run
|
||||
/// it on the same thread it was created on.
|
||||
@@ -1006,6 +1005,56 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<I, O> Action<I, O, LocalStorage>
|
||||
where
|
||||
I: 'static,
|
||||
O: 'static,
|
||||
{
|
||||
/// Creates a new action, which neither requires the action itself nor the
|
||||
/// value it returns to be `Send`. If this action is accessed from outside the
|
||||
/// thread on which it was created, it panics.
|
||||
///
|
||||
/// This combines the features of [`Action::new_local`] and [`Action::new_unsync`].
|
||||
#[track_caller]
|
||||
pub fn new_unsync_local<F, Fu>(action_fn: F) -> Self
|
||||
where
|
||||
F: Fn(&I) -> Fu + 'static,
|
||||
Fu: Future<Output = O> + 'static,
|
||||
{
|
||||
Self {
|
||||
inner: ArenaItem::new_with_storage(ArcAction::new_unsync(
|
||||
action_fn,
|
||||
)),
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: Location::caller(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new action, which neither requires the action itself nor the
|
||||
/// value it returns to be `Send`, and provides it with an initial value.
|
||||
/// If this action is accessed from outside the thread on which it was created, it panics.
|
||||
///
|
||||
/// This combines the features of [`Action::new_local_with_value`] and
|
||||
/// [`Action::new_unsync_with_value`].
|
||||
#[track_caller]
|
||||
pub fn new_unsync_local_with_value<F, Fu>(
|
||||
value: Option<O>,
|
||||
action_fn: F,
|
||||
) -> Self
|
||||
where
|
||||
F: Fn(&I) -> Fu + 'static,
|
||||
Fu: Future<Output = O> + 'static,
|
||||
{
|
||||
Self {
|
||||
inner: ArenaItem::new_with_storage(
|
||||
ArcAction::new_unsync_with_value(value, action_fn),
|
||||
),
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: Location::caller(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<I, O, S> DefinedAt for Action<I, O, S> {
|
||||
fn defined_at(&self) -> Option<&'static Location<'static>> {
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user