mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 10:11:56 -05:00
Compare commits
3 Commits
4079
...
websocket-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71a2534fad | ||
|
|
b5e86886e6 | ||
|
|
d7fbe615cd |
9
.github/ISSUE_TEMPLATE/bug_report.md
vendored
9
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -33,11 +33,10 @@ Steps to reproduce the behavior:
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Next Steps**
|
||||
|
||||
- [ ] I will make a PR
|
||||
- [ ] I would like to make a PR, but need help getting started
|
||||
- [ ] I want someone else to take the time to fix this
|
||||
- [ ] This is a low priority for me and is just shared for your information
|
||||
[ ] I will make a PR
|
||||
[ ] I would like to make a PR, but need help getting started
|
||||
[ ] I want someone else to take the time to fix this
|
||||
[ ] This is a low priority for me and is just shared for your information
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
||||
19
.github/dependabot.yml
vendored
19
.github/dependabot.yml
vendored
@@ -1,19 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
# Grouping all dependencies in one PR weekly
|
||||
- package-ecosystem: cargo
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: monday
|
||||
open-pull-requests-limit: 1
|
||||
allow:
|
||||
- dependency-type: "all"
|
||||
groups:
|
||||
rust-dependencies:
|
||||
patterns:
|
||||
- "*"
|
||||
28
.github/workflows/autofix.yml
vendored
28
.github/workflows/autofix.yml
vendored
@@ -21,19 +21,33 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with: {toolchain: "nightly-2025-04-16", components: "rustfmt, clippy", target: "wasm32-unknown-unknown", rustflags: ""}
|
||||
with: {toolchain: nightly, components: "rustfmt, clippy", target: "wasm32-unknown-unknown", rustflags: ""}
|
||||
- name: Install Glib
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libglib2.0-dev
|
||||
- name: Install cargo-all-features
|
||||
run: cargo install --git https://github.com/sabify/cargo-all-features --branch arbitrary-command-support
|
||||
- name: Install jq
|
||||
run: sudo apt-get install jq
|
||||
- name: Format the workspace
|
||||
run: cargo fmt --all
|
||||
- name: Clippy the workspace
|
||||
run: cargo all-features clippy --allow-dirty --fix --lib --no-deps
|
||||
- run: |
|
||||
echo "Formatting the workspace"
|
||||
cargo fmt --all
|
||||
|
||||
echo "Running Clippy against each member's features (default features included)"
|
||||
for member in $(cargo metadata --no-deps --format-version 1 | jq -r '.packages[] | .name'); do
|
||||
echo "Working on member $member":
|
||||
echo -e "\tdefault-features/no-features:"
|
||||
# this will also run on members with no features or default features
|
||||
cargo clippy --allow-dirty --fix --lib --package "$member"
|
||||
|
||||
features=$(cargo metadata --no-deps --format-version 1 | jq -r ".packages[] | select(.name == \"$member\") | .features | keys[]")
|
||||
for feature in $features; do
|
||||
if [ "$feature" = "default" ]; then
|
||||
continue
|
||||
fi
|
||||
echo -e "\tfeature $feature"
|
||||
cargo clippy --allow-dirty --fix --lib --package "$member" --features "$feature"
|
||||
done
|
||||
done
|
||||
- uses: autofix-ci/action@v1.3.1
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
|
||||
20
.github/workflows/run-cargo-make-task.yml
vendored
20
.github/workflows/run-cargo-make-task.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
toolchain: [stable, nightly-2025-04-16]
|
||||
toolchain: [stable, nightly-2025-03-05]
|
||||
erased_mode: [true, false]
|
||||
steps:
|
||||
- name: Free Disk Space
|
||||
@@ -72,14 +72,6 @@ jobs:
|
||||
run: cargo binstall cargo-nextest --no-confirm
|
||||
- name: Install cargo-all-features
|
||||
run: cargo install --git https://github.com/sabify/cargo-all-features --branch arbitrary-command-support
|
||||
# Part of direct-minimal-versions check
|
||||
- name: Install cargo-hack
|
||||
if: contains(matrix.toolchain, 'nightly')
|
||||
uses: taiki-e/install-action@cargo-hack
|
||||
# Part of direct-minimal-versions check
|
||||
- name: Install cargo-minimal-versions
|
||||
if: contains(matrix.toolchain, 'nightly')
|
||||
uses: taiki-e/install-action@cargo-minimal-versions
|
||||
- name: Install Trunk
|
||||
if: contains(inputs.directory, 'examples')
|
||||
run: cargo binstall trunk --no-confirm
|
||||
@@ -168,16 +160,6 @@ jobs:
|
||||
run: |
|
||||
cd '${{ inputs.directory }}'
|
||||
cargo make --no-workspace --profile=github-actions ci
|
||||
# check the direct-minimal-versions on release
|
||||
if [[ "${{ github.ref_name }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
cargo make --no-workspace --profile=github-actions check-minimal-versions
|
||||
fi
|
||||
# Check if the counter_isomorphic can be built with leptos_debuginfo cfg flag in release mode
|
||||
- name: ${{ inputs.cargo_make_task }} with --cfg=leptos_debuginfo
|
||||
if: contains(inputs.directory, 'counter_isomorphic')
|
||||
run: |
|
||||
cd '${{ inputs.directory }}'
|
||||
RUSTFLAGS="$RUSTFLAGS --cfg leptos_debuginfo" cargo leptos build --release
|
||||
- name: Clean up ${{ inputs.directory }}
|
||||
if: always()
|
||||
run: |
|
||||
|
||||
888
Cargo.lock
generated
888
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
142
Cargo.toml
142
Cargo.toml
@@ -40,132 +40,44 @@ members = [
|
||||
exclude = ["benchmarks", "examples", "projects"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.8.2"
|
||||
version = "0.8.0-rc3"
|
||||
edition = "2021"
|
||||
rust-version = "1.76"
|
||||
|
||||
[workspace.dependencies]
|
||||
# members
|
||||
convert_case = "0.8"
|
||||
throw_error = { path = "./any_error/", version = "0.3.0" }
|
||||
any_spawner = { path = "./any_spawner/", version = "0.3.0" }
|
||||
any_spawner = { path = "./any_spawner/", version = "0.3.0-rc3" }
|
||||
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1" }
|
||||
either_of = { path = "./either_of/", version = "0.1.5" }
|
||||
hydration_context = { path = "./hydration_context", version = "0.3.0" }
|
||||
leptos = { path = "./leptos", version = "0.8.2" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.8.2" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.8.2" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.2" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.8.2" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.8.2" }
|
||||
leptos_router = { path = "./router", version = "0.8.2" }
|
||||
leptos_router_macro = { path = "./router_macro", version = "0.8.2" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.8.2" }
|
||||
leptos_meta = { path = "./meta", version = "0.8.2" }
|
||||
itertools = "0.14.0"
|
||||
leptos = { path = "./leptos", version = "0.8.0-rc3" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.8.0-rc3" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.8.0-rc3" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.0-rc3" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.8.0-rc3" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.8.0-rc3" }
|
||||
leptos_router = { path = "./router", version = "0.8.0-rc3" }
|
||||
leptos_router_macro = { path = "./router_macro", version = "0.8.0-rc3" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.8.0-rc3" }
|
||||
leptos_meta = { path = "./meta", version = "0.8.0-rc3" }
|
||||
next_tuple = { path = "./next_tuple", version = "0.1.0" }
|
||||
oco_ref = { path = "./oco", version = "0.2.0" }
|
||||
or_poisoned = { path = "./or_poisoned", version = "0.1.0" }
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.2.0" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.2.0" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.2.0" }
|
||||
server_fn = { path = "./server_fn", version = "0.8.2" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.8.2" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.2" }
|
||||
tachys = { path = "./tachys", version = "0.2.0" }
|
||||
|
||||
# members deps
|
||||
itertools = { default-features = false, version = "0.14.0" }
|
||||
convert_case = { default-features = false, version = "0.8.0" }
|
||||
serde_json = { default-features = false, version = "1.0.140" }
|
||||
trybuild = { default-features = false, version = "1.0.105" }
|
||||
typed-builder = { default-features = false, version = "0.21.0" }
|
||||
thiserror = { default-features = false, version = "2.0.12" }
|
||||
wasm-bindgen = { default-features = false, version = "0.2.100" }
|
||||
indexmap = { default-features = false, version = "2.9.0" }
|
||||
rstml = { default-features = false, version = "0.12.1" }
|
||||
rustc_version = { default-features = false, version = "0.4.1" }
|
||||
guardian = { default-features = false, version = "1.3.0" }
|
||||
rustc-hash = { default-features = false, version = "2.1.1" }
|
||||
once_cell = { default-features = false, version = "1.21.3" }
|
||||
actix-web = { default-features = false, version = "4.11.0" }
|
||||
tracing = { default-features = false, version = "0.1.41" }
|
||||
slotmap = { default-features = false, version = "1.0.7" }
|
||||
futures = { default-features = false, version = "0.3.31" }
|
||||
dashmap = { default-features = false, version = "6.1.0" }
|
||||
pin-project-lite = { default-features = false, version = "0.2.16" }
|
||||
send_wrapper = { default-features = false, version = "0.6.0" }
|
||||
tokio-test = { default-features = false, version = "0.4.4" }
|
||||
html-escape = { default-features = false, version = "0.2.13" }
|
||||
proc-macro-error2 = { default-features = false, version = "2.0.1" }
|
||||
const_format = { default-features = false, version = "0.2.34" }
|
||||
gloo-net = { default-features = false, version = "0.6.0" }
|
||||
url = { default-features = false, version = "2.5.4" }
|
||||
tokio = { default-features = false, version = "1.45.1" }
|
||||
base64 = { default-features = false, version = "0.22.1" }
|
||||
cfg-if = { default-features = false, version = "1.0.0" }
|
||||
wasm-bindgen-futures = { default-features = false, version = "0.4.50" }
|
||||
tower = { default-features = false, version = "0.5.2" }
|
||||
proc-macro2 = { default-features = false, version = "1.0.95" }
|
||||
serde = { default-features = false, version = "1.0.219" }
|
||||
parking_lot = { default-features = false, version = "0.12.4" }
|
||||
axum = { default-features = false, version = "0.8.4" }
|
||||
serde_qs = { default-features = false, version = "0.15.0" }
|
||||
syn = { default-features = false, version = "2.0.101" }
|
||||
xxhash-rust = { default-features = false, version = "0.8.15" }
|
||||
paste = { default-features = false, version = "1.0.15" }
|
||||
quote = { default-features = false, version = "1.0.40" }
|
||||
web-sys = { default-features = false, version = "0.3.77" }
|
||||
js-sys = { default-features = false, version = "0.3.77" }
|
||||
rand = { default-features = false, version = "0.9.1" }
|
||||
serde-lite = { default-features = false, version = "0.5.0" }
|
||||
tokio-tungstenite = { default-features = false, version = "0.26.2" }
|
||||
serial_test = { default-features = false, version = "3.2.0" }
|
||||
erased = { default-features = false, version = "0.1.2" }
|
||||
glib = { default-features = false, version = "0.20.10" }
|
||||
async-trait = { default-features = false, version = "0.1.88" }
|
||||
typed-builder-macro = { default-features = false, version = "0.21.0" }
|
||||
linear-map = { default-features = false, version = "1.2.0" }
|
||||
anyhow = { default-features = false, version = "1.0.98" }
|
||||
walkdir = { default-features = false, version = "2.5.0" }
|
||||
actix-ws = { default-features = false, version = "0.3.0" }
|
||||
tower-http = { default-features = false, version = "0.6.4" }
|
||||
prettyplease = { default-features = false, version = "0.2.33" }
|
||||
inventory = { default-features = false, version = "0.3.20" }
|
||||
config = { default-features = false, version = "0.15.11" }
|
||||
camino = { default-features = false, version = "1.1.9" }
|
||||
ciborium = { default-features = false, version = "0.2.2" }
|
||||
multer = { default-features = false, version = "3.1.0" }
|
||||
leptos-spin-macro = { default-features = false, version = "0.2.0" }
|
||||
sledgehammer_utils = { default-features = false, version = "0.3.1" }
|
||||
sledgehammer_bindgen = { default-features = false, version = "0.6.0" }
|
||||
wasm-streams = { default-features = false, version = "0.4.2" }
|
||||
rkyv = { default-features = false, version = "0.8.10" }
|
||||
temp-env = { default-features = false, version = "0.3.6" }
|
||||
uuid = { default-features = false, version = "1.17.0" }
|
||||
bytes = { default-features = false, version = "1.10.1" }
|
||||
http = { default-features = false, version = "1.3.1" }
|
||||
regex = { default-features = false, version = "1.11.1" }
|
||||
drain_filter_polyfill = { default-features = false, version = "0.1.3" }
|
||||
tempfile = { default-features = false, version = "3.20.0" }
|
||||
futures-lite = { default-features = false, version = "2.6.0" }
|
||||
log = { default-features = false, version = "0.4.27" }
|
||||
percent-encoding = { default-features = false, version = "2.3.1" }
|
||||
async-executor = { default-features = false, version = "1.13.2" }
|
||||
const-str = { default-features = false, version = "0.6.2" }
|
||||
http-body-util = { default-features = false, version = "0.1.3" }
|
||||
hyper = { default-features = false, version = "1.6.0" }
|
||||
postcard = { default-features = false, version = "1.1.1" }
|
||||
rmp-serde = { default-features = false, version = "1.3.0" }
|
||||
reqwest = { default-features = false, version = "0.12.18" }
|
||||
tower-layer = { default-features = false, version = "0.3.3" }
|
||||
attribute-derive = { default-features = false, version = "0.10.3" }
|
||||
insta = { default-features = false, version = "1.43.1" }
|
||||
codee = { default-features = false, version = "0.3.0" }
|
||||
actix-http = { default-features = false, version = "3.11.0" }
|
||||
wasm-bindgen-test = { default-features = false, version = "0.3.50" }
|
||||
rustversion = { default-features = false, version = "1.0.21" }
|
||||
getrandom = { default-features = false, version = "0.3.3" }
|
||||
actix-files = { default-features = false, version = "0.6.6" }
|
||||
async-lock = { default-features = false, version = "3.4.0" }
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.2.0-rc3" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.2.0-rc3" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.2.0-rc3" }
|
||||
rustversion = "1"
|
||||
serde_json = "1.0.0"
|
||||
server_fn = { path = "./server_fn", version = "0.8.0-rc3" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.8.0-rc3" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.0-rc3" }
|
||||
tachys = { path = "./tachys", version = "0.2.0-rc3" }
|
||||
trybuild = "1"
|
||||
typed-builder = "0.21.0"
|
||||
thiserror = "2.0.12"
|
||||
wasm-bindgen = "0.2.100"
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -10,4 +10,4 @@ rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
pin-project-lite = { workspace = true, default-features = true }
|
||||
pin-project-lite = "0.2.15"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "any_spawner"
|
||||
version = "0.3.0"
|
||||
version = "0.3.0-rc3"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -9,25 +9,25 @@ description = "Spawn asynchronous tasks in an executor-independent way."
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
async-executor = { optional = true , workspace = true, default-features = true }
|
||||
futures = { workspace = true, default-features = true }
|
||||
glib = { optional = true , workspace = true, default-features = true }
|
||||
thiserror = { workspace = true , default-features = true }
|
||||
tokio = { optional = true, default-features = false, features = [
|
||||
async-executor = { version = "1.13.1", optional = true }
|
||||
futures = "0.3.31"
|
||||
glib = { version = "0.20.6", optional = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { version = "1.41", optional = true, default-features = false, features = [
|
||||
"rt",
|
||||
] , workspace = true }
|
||||
tracing = { optional = true , workspace = true, default-features = true }
|
||||
wasm-bindgen-futures = { optional = true , workspace = true, default-features = true }
|
||||
] }
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
wasm-bindgen-futures = { version = "0.4.50", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
futures-lite = { default-features = false , workspace = true }
|
||||
tokio = { default-features = false, features = [
|
||||
futures-lite = { version = "2.6.0", default-features = false }
|
||||
tokio = { version = "1.41", default-features = false, features = [
|
||||
"rt",
|
||||
"macros",
|
||||
"time",
|
||||
] , workspace = true }
|
||||
wasm-bindgen-test = { workspace = true, default-features = true }
|
||||
serial_test = { workspace = true, default-features = true }
|
||||
] }
|
||||
wasm-bindgen-test = { version = "0.3.50" }
|
||||
serial_test = "3.2.0"
|
||||
|
||||
[features]
|
||||
async-executor = ["dep:async-executor"]
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
[tasks.check-minimal-versions]
|
||||
condition = { channels = ["nightly"] }
|
||||
command = "cargo"
|
||||
args = [
|
||||
"all-features",
|
||||
"minimal-versions",
|
||||
"check",
|
||||
"--ignore-private",
|
||||
"--detach-path-deps",
|
||||
"--direct",
|
||||
]
|
||||
install_script = '''
|
||||
cargo install --git https://github.com/sabify/cargo-all-features --branch arbitrary-command-support
|
||||
'''
|
||||
@@ -1,8 +1,4 @@
|
||||
extend = [
|
||||
{ path = "./lint.toml" },
|
||||
{ path = "./test.toml" },
|
||||
{ path = "./check-minimal-versions.toml" },
|
||||
]
|
||||
extend = [{ path = "./lint.toml" }, { path = "./test.toml" }]
|
||||
|
||||
[env]
|
||||
RUSTFLAGS = ""
|
||||
|
||||
@@ -31,7 +31,7 @@ pub const fn const_concat(
|
||||
let mut i = 0;
|
||||
|
||||
// have it iterate over bytes manually, because, again,
|
||||
// no mutable references in const fns
|
||||
// no mutable refernces in const fns
|
||||
while i < x.len() {
|
||||
buffer[position] = x[i];
|
||||
position += 1;
|
||||
@@ -59,7 +59,7 @@ pub const fn const_concat_with_prefix(
|
||||
let mut i = 0;
|
||||
|
||||
// have it iterate over bytes manually, because, again,
|
||||
// no mutable references in const fns
|
||||
// no mutable refernces in const fns
|
||||
while i < x.len() {
|
||||
buffer[position] = x[i];
|
||||
position += 1;
|
||||
@@ -116,7 +116,7 @@ pub const fn const_concat_with_separator(
|
||||
let mut i = 0;
|
||||
|
||||
// have it iterate over bytes manually, because, again,
|
||||
// no mutable references in const fns
|
||||
// no mutable refernces in const fns
|
||||
while i < x.len() {
|
||||
buffer[position] = x[i];
|
||||
position += 1;
|
||||
|
||||
@@ -10,8 +10,8 @@ rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
pin-project-lite = { workspace = true, default-features = true }
|
||||
paste = { workspace = true, default-features = true }
|
||||
pin-project-lite = "0.2.16"
|
||||
paste = "1.0.15"
|
||||
|
||||
[features]
|
||||
default = ["no_std"]
|
||||
|
||||
@@ -7,7 +7,7 @@ pub fn main() {
|
||||
|
||||
fmt()
|
||||
.with_writer(
|
||||
// To avoid trace events in the browser from showing their
|
||||
// To avoide trace events in the browser from showing their
|
||||
// JS backtrace, which is very annoying, in my opinion
|
||||
MakeConsoleWriter::default()
|
||||
.map_trace_level_to(tracing::Level::DEBUG),
|
||||
|
||||
@@ -6,7 +6,7 @@ use leptos_router::{components::A, hooks::use_params_map};
|
||||
#[component]
|
||||
pub fn Story() -> impl IntoView {
|
||||
let params = use_params_map();
|
||||
let story = Resource::new_blocking(
|
||||
let story = Resource::new(
|
||||
move || params.read().get("id").unwrap_or_default(),
|
||||
move |id| async move {
|
||||
if id.is_empty() {
|
||||
|
||||
@@ -6,7 +6,7 @@ use leptos_router::{components::A, hooks::use_params_map};
|
||||
#[component]
|
||||
pub fn Story() -> impl IntoView {
|
||||
let params = use_params_map();
|
||||
let story = Resource::new_blocking(
|
||||
let story = Resource::new(
|
||||
move || params.read().get("id").unwrap_or_default(),
|
||||
move |id| async move {
|
||||
if id.is_empty() {
|
||||
|
||||
@@ -13,7 +13,7 @@ pub async fn fetch_story(
|
||||
#[component]
|
||||
pub fn Story() -> impl IntoView {
|
||||
let params = use_params_map();
|
||||
let story = Resource::new_blocking(
|
||||
let story = Resource::new(
|
||||
move || params.read().get("id").unwrap_or_default(),
|
||||
move |id| async move {
|
||||
if id.is_empty() {
|
||||
|
||||
@@ -7,7 +7,7 @@ use send_wrapper::SendWrapper;
|
||||
#[component]
|
||||
pub fn Story() -> impl IntoView {
|
||||
let params = use_params_map();
|
||||
let story = Resource::new_blocking(
|
||||
let story = Resource::new(
|
||||
move || params.read().get("id").unwrap_or_default(),
|
||||
move |id| {
|
||||
SendWrapper::new(async move {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Stores Example
|
||||
# Leptos Counter Example
|
||||
|
||||
This example shows how to use reactive stores, by building a client-side rendered TODO application.
|
||||
This example creates a simple counter in a client side rendered app with Rust and WASM!
|
||||
|
||||
## Getting Started
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ Feature: Using instrumented counters to test regression from #3502.
|
||||
| list_items | 1 |
|
||||
| get_item | 1 |
|
||||
| inspect_item_root | 0 |
|
||||
| inspect_item_field | 3 |
|
||||
| inspect_item_field | 4 |
|
||||
|
||||
Scenario: Follow paths ordinarily down to a target
|
||||
Given I select the following links
|
||||
|
||||
@@ -477,8 +477,6 @@ fn ItemInspect() -> impl IntoView {
|
||||
move || params.get().map(|p| p.path),
|
||||
move |p| async move {
|
||||
// leptos::logging::log!("res_inspect: res_overview.await");
|
||||
// Note: this resource is untracked here, though `params` changing
|
||||
// will nonetheless results in the "expected" tracked updates.
|
||||
let overview = res_overview.await;
|
||||
// leptos::logging::log!("res_inspect: resolved res_overview.await");
|
||||
// let result =
|
||||
@@ -563,7 +561,7 @@ fn ShowCounters() -> impl IntoView {
|
||||
//
|
||||
// However, upon `Reset Counters`, the mode from which the reset
|
||||
// was issued will result in the rendering be reflected as such, so
|
||||
// if the initial state was SSR, resetting under CSR will result in
|
||||
// if the intial state was SSR, resetting under CSR will result in
|
||||
// the CSR counters be rendered after. However for the intents and
|
||||
// purpose for the testing only the CSR is cared for.
|
||||
//
|
||||
|
||||
10
examples/websocket/e2e/tests/fixtures/action.rs
vendored
10
examples/websocket/e2e/tests/fixtures/action.rs
vendored
@@ -10,9 +10,19 @@ pub async fn goto_path(client: &Client, path: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn add_text(client: &Client, text: &str) -> Result<String> {
|
||||
fill_input(client, text).await?;
|
||||
get_label(client).await
|
||||
}
|
||||
|
||||
pub async fn fill_input(client: &Client, text: &str) -> Result<()> {
|
||||
let textbox = find::input(client).await;
|
||||
textbox.send_keys(text).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_label(client: &Client) -> Result<String> {
|
||||
let label = find::label(client).await;
|
||||
Ok(label.text().await?)
|
||||
}
|
||||
|
||||
10
examples/websocket/e2e/tests/fixtures/find.rs
vendored
10
examples/websocket/e2e/tests/fixtures/find.rs
vendored
@@ -9,3 +9,13 @@ pub async fn input(client: &Client) -> Element {
|
||||
|
||||
textbox
|
||||
}
|
||||
|
||||
pub async fn label(client: &Client) -> Element {
|
||||
let label = client
|
||||
.wait()
|
||||
.for_element(Locator::Css("p"))
|
||||
.await
|
||||
.expect("");
|
||||
|
||||
label
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ async fn i_open_the_app(world: &mut AppWorld) -> Result<()> {
|
||||
#[given(regex = "^I add a text as (.*)$")]
|
||||
async fn i_add_a_text(world: &mut AppWorld, text: String) -> Result<()> {
|
||||
let client = &world.client;
|
||||
action::fill_input(client, text.as_str()).await?;
|
||||
action::add_text(client, text.as_str()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ async fn i_see_the_label_of_the_input_is(
|
||||
world: &mut AppWorld,
|
||||
text: String,
|
||||
) -> Result<()> {
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
let client = &world.client;
|
||||
check::text_on_element(client, "p", &text).await?;
|
||||
|
||||
|
||||
4
examples/websocket_shopping_list/.dockerignore
Normal file
4
examples/websocket_shopping_list/.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
||||
# flyctl launch added from .gitignore
|
||||
**/Dockerfile
|
||||
**/target
|
||||
fly.toml
|
||||
2
examples/websocket_shopping_list/.gitignore
vendored
Normal file
2
examples/websocket_shopping_list/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
Dockerfile
|
||||
target
|
||||
83
examples/websocket_shopping_list/Cargo.toml
Normal file
83
examples/websocket_shopping_list/Cargo.toml
Normal file
@@ -0,0 +1,83 @@
|
||||
[package]
|
||||
name = "shopping_list"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
console_log = "1.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.30"
|
||||
#leptos = { path = "../../leptos", features = ["tracing"] }
|
||||
#leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
#reactive_stores = { path = "../../reactive_stores" }
|
||||
leptos = { version = "0.8", features = ["tracing"] }
|
||||
leptos_axum = { version = "0.8", optional = true }
|
||||
leptos_meta = { version = "0.8" }
|
||||
reactive_stores = { version = "0.2" }
|
||||
log = "0.4.22"
|
||||
simple_logger = "5.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
thiserror = "2.0"
|
||||
wasm-bindgen = "0.2.100"
|
||||
uuid = { version = "1.16.0", features = ["v4", "serde"] }
|
||||
async-broadcast = "0.7.2"
|
||||
|
||||
[features]
|
||||
hydrate = ["leptos/hydrate", "uuid/js"]
|
||||
ssr = ["dep:axum", "dep:tokio", "leptos/ssr", "dep:leptos_axum"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["axum", "tokio", "leptos_axum"]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"], []]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
output-name = "shopping_list"
|
||||
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
|
||||
site-root = "target/site"
|
||||
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
|
||||
# Defaults to pkg
|
||||
site-pkg-dir = "pkg"
|
||||
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
|
||||
style-file = "./style.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"
|
||||
# The port to use for automatic reload monitoring
|
||||
reload-port = 3001
|
||||
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
||||
end2end-cmd = "cargo make test-ui"
|
||||
end2end-dir = "e2e"
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
browserquery = "defaults"
|
||||
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
|
||||
watch = false
|
||||
# The environment Leptos will run in, usually either "DEV" or "PROD"
|
||||
env = "DEV"
|
||||
# The features to use when compiling the bin target
|
||||
#
|
||||
# Optional. Can be over-ridden with the command line parameter --bin-features
|
||||
bin-features = ["ssr"]
|
||||
|
||||
# If the --no-default-features flag should be used when compiling the bin target
|
||||
#
|
||||
# Optional. Defaults to false.
|
||||
bin-default-features = false
|
||||
|
||||
# The features to use when compiling the lib target
|
||||
#
|
||||
# Optional. Can be over-ridden with the command line parameter --lib-features
|
||||
lib-features = ["hydrate"]
|
||||
|
||||
# If the --no-default-features flag should be used when compiling the lib target
|
||||
#
|
||||
# Optional. Defaults to false.
|
||||
lib-default-features = false
|
||||
|
||||
hash-files = true
|
||||
21
examples/websocket_shopping_list/LICENSE
Normal file
21
examples/websocket_shopping_list/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Greg Johnston
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
12
examples/websocket_shopping_list/Makefile.toml
Normal file
12
examples/websocket_shopping_list/Makefile.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/cargo-leptos-webdriver-test.toml" },
|
||||
]
|
||||
|
||||
[env]
|
||||
CLIENT_PROCESS_NAME = "websocket"
|
||||
|
||||
[tasks.test-ui]
|
||||
cwd = "./e2e"
|
||||
command = "cargo"
|
||||
args = ["make", "test-ui", "${@}"]
|
||||
19
examples/websocket_shopping_list/README.md
Normal file
19
examples/websocket_shopping_list/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Leptos WebSocket
|
||||
|
||||
This example creates a basic WebSocket echo app.
|
||||
|
||||
## Getting Started
|
||||
|
||||
See the [Examples README](../README.md) for setup and run instructions.
|
||||
|
||||
## E2E Testing
|
||||
|
||||
See the [E2E README](./e2e/README.md) for more information about the testing strategy.
|
||||
|
||||
## Rendering
|
||||
|
||||
See the [SSR Notes](../SSR_NOTES.md) for more information about Server Side Rendering.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Run `cargo leptos watch` to run this example.
|
||||
22
examples/websocket_shopping_list/fly.toml
Normal file
22
examples/websocket_shopping_list/fly.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
# fly.toml app configuration file generated for leptos-shopping on 2025-05-09T11:29:06-04:00
|
||||
#
|
||||
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
|
||||
#
|
||||
|
||||
app = 'leptos-shopping'
|
||||
primary_region = 'bos'
|
||||
|
||||
[build]
|
||||
|
||||
[http_service]
|
||||
internal_port = 8080
|
||||
force_https = true
|
||||
auto_stop_machines = 'stop'
|
||||
auto_start_machines = true
|
||||
min_machines_running = 0
|
||||
processes = ['app']
|
||||
|
||||
[[vm]]
|
||||
memory = '1gb'
|
||||
cpu_kind = 'shared'
|
||||
cpus = 1
|
||||
BIN
examples/websocket_shopping_list/public/favicon.ico
Normal file
BIN
examples/websocket_shopping_list/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
375
examples/websocket_shopping_list/src/app.rs
Normal file
375
examples/websocket_shopping_list/src/app.rs
Normal file
@@ -0,0 +1,375 @@
|
||||
use futures::channel::mpsc::UnboundedSender;
|
||||
use leptos::{html::Input, prelude::*, task::spawn_local};
|
||||
use leptos_meta::HashedStylesheet;
|
||||
use reactive_stores::{ArcStore, Field, Store, StoreFieldIterator};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use server_fn::{codec::JsonEncoding, BoxedStream, ServerFnError, Websocket};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
view! {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<AutoReload options=options.clone() />
|
||||
<HashedStylesheet options=options.clone()/>
|
||||
<HydrationScripts options />
|
||||
<link rel="shortcut icon" type="image/ico" href="/favicon.ico" />
|
||||
</head>
|
||||
<body>
|
||||
<App />
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
}
|
||||
|
||||
// Business Logic
|
||||
|
||||
#[derive(Debug, Default, Clone, Store, PartialEq, Eq)]
|
||||
pub struct ShoppingList {
|
||||
#[store(key: Uuid = |item| item.id)]
|
||||
pub items: Vec<Item>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Store, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Item {
|
||||
pub id: Uuid,
|
||||
pub label: String,
|
||||
pub completed: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Message {
|
||||
Connect,
|
||||
Disconnect,
|
||||
Welcome { list: Vec<Item> },
|
||||
Add { id: Uuid, label: String },
|
||||
Remove { id: Uuid },
|
||||
MarkComplete { id: Uuid, completed: bool },
|
||||
Edit { id: Uuid, new_label: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Client {
|
||||
store: State,
|
||||
connection:
|
||||
StoredValue<UnboundedSender<Result<MessageWithUser, ServerFnError>>>,
|
||||
user: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct State(Store<ShoppingList>);
|
||||
|
||||
impl From<ArcStore<ShoppingList>> for State {
|
||||
fn from(value: ArcStore<ShoppingList>) -> Self {
|
||||
State(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Applies an update to the local store.
|
||||
pub fn apply_local_update(&self, message: Message) {
|
||||
match message {
|
||||
Message::Connect => {}
|
||||
Message::Disconnect => {}
|
||||
Message::Welcome { list } => *self.0.items().write() = list,
|
||||
Message::Add { id, label } => self.0.items().write().push(Item {
|
||||
id,
|
||||
label,
|
||||
completed: false,
|
||||
}),
|
||||
Message::Remove { id } => {
|
||||
self.0.items().write().retain(|item| item.id != id);
|
||||
}
|
||||
Message::MarkComplete { id, completed } => {
|
||||
if let Some(item) = self.find(&id) {
|
||||
*item.completed().write() = completed;
|
||||
}
|
||||
}
|
||||
Message::Edit { id, new_label } => {
|
||||
if let Some(item) = self.find(&id) {
|
||||
*item.label().write() = new_label;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn find(&self, id: &Uuid) -> Option<Field<Item>> {
|
||||
let store = self.0.items().read_untracked();
|
||||
store
|
||||
.iter()
|
||||
.position(|item| &item.id == id)
|
||||
.map(|idx| self.0.items().at_unkeyed(idx).into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new(
|
||||
connection: UnboundedSender<Result<MessageWithUser, ServerFnError>>,
|
||||
) -> Self {
|
||||
let user = Uuid::new_v4();
|
||||
connection
|
||||
.unbounded_send(Ok((user, Message::Connect)))
|
||||
.unwrap();
|
||||
|
||||
Self {
|
||||
user,
|
||||
store: State(Store::new(ShoppingList::default())),
|
||||
connection: StoredValue::new(connection),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn goodbye(&self) {
|
||||
_ = self
|
||||
.connection
|
||||
.read_value()
|
||||
.unbounded_send(Ok((self.user, Message::Disconnect)));
|
||||
}
|
||||
|
||||
/// Updates the shopping list from this local device. This will both
|
||||
/// update the state of the UI here, and send the update over the websocket.
|
||||
pub fn update(&self, message: Message) {
|
||||
self.store.apply_local_update(message.clone());
|
||||
self.send_update(message);
|
||||
}
|
||||
|
||||
/// Applies an update that was received from the server.
|
||||
pub fn received_update(&self, user: Uuid, message: Message) {
|
||||
match message {
|
||||
Message::Welcome { list } => {
|
||||
*self.store.0.items().write() = list;
|
||||
}
|
||||
_ => {
|
||||
if user != self.user {
|
||||
self.store.apply_local_update(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends an update to the server.
|
||||
pub fn send_update(&self, message: Message) {
|
||||
self.connection
|
||||
.read_value()
|
||||
.unbounded_send(Ok((self.user, message)))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
type MessageWithUser = (Uuid, Message);
|
||||
|
||||
#[server(protocol = Websocket<JsonEncoding, JsonEncoding>)]
|
||||
async fn messages(
|
||||
input: BoxedStream<MessageWithUser, ServerFnError>,
|
||||
) -> Result<BoxedStream<MessageWithUser, ServerFnError>, ServerFnError> {
|
||||
let mut input = input; // FIXME :-)
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ServerState(ArcStore<ShoppingList>);
|
||||
|
||||
impl ServerState {
|
||||
fn initial_items(&self) -> Vec<Item> {
|
||||
self.0.clone().items().get_untracked()
|
||||
}
|
||||
|
||||
fn apply_local_update(&self, message: Message) {
|
||||
Owner::new().with(|| {
|
||||
State::from(self.0.clone()).apply_local_update(message)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
use futures::{
|
||||
channel::mpsc::{channel, Sender},
|
||||
StreamExt,
|
||||
};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{LazyLock, Mutex},
|
||||
};
|
||||
|
||||
static SHOPPING_LIST: LazyLock<ServerState> =
|
||||
LazyLock::new(|| ServerState(ArcStore::new(ShoppingList::default())));
|
||||
static USER_SENDERS: LazyLock<
|
||||
Mutex<HashMap<Uuid, Sender<Result<MessageWithUser, ServerFnError>>>>,
|
||||
> = LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
let (tx, rx) = channel(32);
|
||||
let mut tx = Some(tx);
|
||||
|
||||
// spawn a task to listen to the input stream of messages coming in over the websocket
|
||||
tokio::spawn(async move {
|
||||
while let Some(msg) = input.next().await {
|
||||
match msg {
|
||||
Err(e) => eprintln!("{e}"),
|
||||
Ok((user, msg)) => match msg {
|
||||
Message::Connect => {
|
||||
println!("\nuser connecting: {user:?}");
|
||||
if let Some(mut tx) = tx.take() {
|
||||
tx.try_send(Ok((
|
||||
user,
|
||||
Message::Welcome {
|
||||
list: SHOPPING_LIST.initial_items(),
|
||||
},
|
||||
)))
|
||||
.unwrap();
|
||||
USER_SENDERS.lock().unwrap().insert(user, tx);
|
||||
}
|
||||
}
|
||||
Message::Disconnect => {
|
||||
println!("\nuser disconnecting: {user:?}");
|
||||
USER_SENDERS.lock().unwrap().remove(&user);
|
||||
}
|
||||
_ => {
|
||||
println!("\nmsg from {user:?} {msg:?}");
|
||||
|
||||
SHOPPING_LIST.apply_local_update(msg.clone());
|
||||
|
||||
let mut senders = USER_SENDERS.lock().unwrap();
|
||||
senders.retain(|tx_user, tx| {
|
||||
if tx_user != &user {
|
||||
let res = tx.try_send(Ok((user, msg.clone())));
|
||||
if res.is_err() {
|
||||
println!("user disconnected: {tx_user:?}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
});
|
||||
|
||||
println!("\n{:#?}", &*SHOPPING_LIST.0.read_untracked());
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(rx.into())
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
use futures::{channel::mpsc, StreamExt};
|
||||
let (tx, rx) = mpsc::unbounded();
|
||||
|
||||
let client = Client::new(tx);
|
||||
|
||||
// we'll only listen for websocket messages on the client
|
||||
if cfg!(feature = "hydrate") {
|
||||
on_cleanup(move || {
|
||||
client.goodbye();
|
||||
});
|
||||
|
||||
spawn_local(async move {
|
||||
match messages(rx.into()).await {
|
||||
Ok(mut messages) => {
|
||||
while let Some(msg) = messages.next().await {
|
||||
leptos::logging::log!("{:?}", msg);
|
||||
match msg {
|
||||
Ok((user, msg)) => {
|
||||
// when we get a message from the server, only apply it locally
|
||||
client.received_update(user, msg);
|
||||
}
|
||||
Err(e) => {
|
||||
leptos::logging::error!("{e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => leptos::logging::warn!("{e}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let add_item = NodeRef::<Input>::new();
|
||||
|
||||
view! {
|
||||
<h1>"My Shopping List"</h1>
|
||||
<form
|
||||
class="add"
|
||||
on:submit:target=move |ev| {
|
||||
ev.prevent_default();
|
||||
let label = add_item.get().unwrap().value();
|
||||
client.update(Message::Add { id: Uuid::new_v4(), label });
|
||||
ev.target().reset();
|
||||
}
|
||||
>
|
||||
<input type="text" node_ref=add_item autofocus/>
|
||||
<input
|
||||
type="submit"
|
||||
value="Add"
|
||||
/>
|
||||
</form>
|
||||
<ul>
|
||||
<For
|
||||
each=move || client.store.0.items()
|
||||
key=|item| item.id().get()
|
||||
let:item
|
||||
>
|
||||
<ItemEditor client item/>
|
||||
</For>
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn ItemEditor(
|
||||
client: Client,
|
||||
#[prop(into)] item: Field<Item>,
|
||||
) -> impl IntoView {
|
||||
let editing = RwSignal::new(false);
|
||||
|
||||
view! {
|
||||
<li class:completed=item.completed()>
|
||||
<input
|
||||
class="item"
|
||||
type="checkbox"
|
||||
prop:checked=item.completed()
|
||||
id=move || item.id().read().to_string()
|
||||
on:change:target=move |ev| {
|
||||
client.update(Message::MarkComplete {
|
||||
id: item.id().get(),
|
||||
completed: ev.target().checked()
|
||||
});
|
||||
}
|
||||
/>
|
||||
<label
|
||||
class="item"
|
||||
class:hidden=move || editing.get()
|
||||
for=move || item.id().read().to_string()
|
||||
>
|
||||
{item.label()}
|
||||
</label>
|
||||
<input
|
||||
class="item"
|
||||
type="text"
|
||||
prop:value=item.label()
|
||||
on:change:target=move |ev| {
|
||||
client.update(Message::Edit {
|
||||
id: item.id().get(),
|
||||
new_label: ev.target().value()
|
||||
});
|
||||
editing.set(false);
|
||||
}
|
||||
class:hidden=move || !editing.get()
|
||||
/>
|
||||
<button
|
||||
class:hidden=move || editing.get()
|
||||
on:click=move |_| editing.set(true)
|
||||
>
|
||||
"Edit"
|
||||
</button>
|
||||
<button
|
||||
class:hidden=move || !editing.get()
|
||||
on:click=move |_| editing.set(false)
|
||||
>
|
||||
"Cancel"
|
||||
</button>
|
||||
<button on:click=move |_| client.update(Message::Remove { id: item.id().get() })>
|
||||
"✕"
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
9
examples/websocket_shopping_list/src/lib.rs
Normal file
9
examples/websocket_shopping_list/src/lib.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
pub mod app;
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
use crate::app::App;
|
||||
console_error_panic_hook::set_once();
|
||||
leptos::mount::hydrate_body(App);
|
||||
}
|
||||
34
examples/websocket_shopping_list/src/main.rs
Normal file
34
examples/websocket_shopping_list/src/main.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use axum::Router;
|
||||
use leptos::prelude::*;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use shopping_list::app::{shell, App};
|
||||
|
||||
simple_logger::init_with_level(log::Level::Error)
|
||||
.expect("couldn't initialize logging");
|
||||
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars
|
||||
let conf = get_configuration(None).unwrap();
|
||||
let leptos_options = conf.leptos_options;
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = generate_route_list(App);
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.leptos_routes(&leptos_options, routes, {
|
||||
let leptos_options = leptos_options.clone();
|
||||
move || shell(leptos_options.clone())
|
||||
})
|
||||
.fallback(leptos_axum::file_and_error_handler(shell))
|
||||
.with_state(leptos_options);
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
println!("listening on http://{}", &addr);
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
90
examples/websocket_shopping_list/style.css
Normal file
90
examples/websocket_shopping_list/style.css
Normal file
@@ -0,0 +1,90 @@
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
form.add {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
form.add > * {
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.completed label {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
background: white;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
form.add {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
form.add input[type="text"] {
|
||||
flex: 1;
|
||||
padding: 0.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
form.add input[type="submit"] {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid #ddd;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
label.item {
|
||||
flex: 1;
|
||||
word-break: break-word;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
input.item.hidden {
|
||||
display: none;
|
||||
}
|
||||
@@ -12,12 +12,12 @@ edition.workspace = true
|
||||
[dependencies]
|
||||
throw_error = { workspace = true }
|
||||
or_poisoned = { workspace = true }
|
||||
futures = { workspace = true, default-features = true }
|
||||
serde = { features = ["derive"] , workspace = true, default-features = true }
|
||||
wasm-bindgen = { workspace = true, optional = true , default-features = true }
|
||||
js-sys = { optional = true , workspace = true, default-features = true }
|
||||
once_cell = { workspace = true, default-features = true }
|
||||
pin-project-lite = { workspace = true, default-features = true }
|
||||
futures = "0.3.31"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
wasm-bindgen = { workspace = true, optional = true }
|
||||
js-sys = { version = "0.3.74", optional = true }
|
||||
once_cell = "1.20"
|
||||
pin-project-lite = "0.2.15"
|
||||
|
||||
[features]
|
||||
browser = ["dep:wasm-bindgen", "dep:js-sys"]
|
||||
|
||||
@@ -9,10 +9,10 @@ rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
actix-http = { workspace = true, default-features = true }
|
||||
actix-files = { workspace = true, default-features = true }
|
||||
actix-web = { workspace = true, default-features = true }
|
||||
futures = { workspace = true, default-features = true }
|
||||
actix-http = "3.9"
|
||||
actix-files = "0.6"
|
||||
actix-web = "4.9"
|
||||
futures = "0.3.31"
|
||||
any_spawner = { workspace = true, features = ["tokio"] }
|
||||
hydration_context = { workspace = true }
|
||||
leptos = { workspace = true, features = ["nonce", "ssr"] }
|
||||
@@ -22,13 +22,13 @@ leptos_meta = { workspace = true, features = ["nonce"] }
|
||||
leptos_router = { workspace = true, features = ["ssr"] }
|
||||
server_fn = { workspace = true, features = ["actix"] }
|
||||
tachys = { workspace = true }
|
||||
serde_json = { workspace = true , default-features = true }
|
||||
parking_lot = { workspace = true, default-features = true }
|
||||
tracing = { optional = true , workspace = true, default-features = true }
|
||||
tokio = { features = ["rt", "fs"] , workspace = true, default-features = true }
|
||||
send_wrapper = { workspace = true, default-features = true }
|
||||
dashmap = { workspace = true, default-features = true }
|
||||
once_cell = { workspace = true, default-features = true }
|
||||
serde_json = { workspace = true }
|
||||
parking_lot = "0.12.3"
|
||||
tracing = { version = "0.1", optional = true }
|
||||
tokio = { version = "1.43", features = ["rt", "fs"] }
|
||||
send_wrapper = "0.6.0"
|
||||
dashmap = "6"
|
||||
once_cell = "1"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--generate-link-to-definition"]
|
||||
|
||||
@@ -4,18 +4,18 @@ authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Axum integrations for the Leptos web framework."
|
||||
version = "0.8.2"
|
||||
version = "0.8.0-rc3"
|
||||
rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
any_spawner = { workspace = true, features = ["tokio"] }
|
||||
hydration_context = { workspace = true }
|
||||
axum = { default-features = false, features = [
|
||||
axum = { version = "0.8.1", default-features = false, features = [
|
||||
"matched-path",
|
||||
] , workspace = true }
|
||||
dashmap = { workspace = true, default-features = true }
|
||||
futures = { workspace = true, default-features = true }
|
||||
] }
|
||||
dashmap = "6"
|
||||
futures = "0.3.31"
|
||||
leptos = { workspace = true, features = ["nonce", "ssr"] }
|
||||
server_fn = { workspace = true, features = ["axum-no-default"] }
|
||||
leptos_macro = { workspace = true, features = ["axum"] }
|
||||
@@ -23,16 +23,16 @@ leptos_meta = { workspace = true, features = ["ssr", "nonce"] }
|
||||
leptos_router = { workspace = true, features = ["ssr"] }
|
||||
leptos_integration_utils = { workspace = true }
|
||||
tachys = { workspace = true }
|
||||
once_cell = { workspace = true, default-features = true }
|
||||
parking_lot = { workspace = true, default-features = true }
|
||||
tokio = { default-features = false , workspace = true }
|
||||
tower = { features = ["util"] , workspace = true, default-features = true }
|
||||
tower-http = { workspace = true, default-features = true }
|
||||
tracing = { optional = true , workspace = true, default-features = true }
|
||||
once_cell = "1"
|
||||
parking_lot = "0.12.3"
|
||||
tokio = { version = "1.43", default-features = false }
|
||||
tower = { version = "0.5.1", features = ["util"] }
|
||||
tower-http = "0.6.2"
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
axum = { workspace = true, default-features = true }
|
||||
tokio = { features = ["net", "rt-multi-thread"] , workspace = true, default-features = true }
|
||||
axum = "0.8.1"
|
||||
tokio = { version = "1.43", features = ["net", "rt-multi-thread"] }
|
||||
|
||||
[features]
|
||||
wasm = []
|
||||
|
||||
@@ -590,7 +590,7 @@ where
|
||||
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
|
||||
/// to route it using [leptos_router], serving an HTML stream of your application.
|
||||
///
|
||||
/// This version allows us to pass Axum State/Extension/Extractor or other info from Axum or network
|
||||
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network
|
||||
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
|
||||
/// the data to leptos in a closure. An example is below
|
||||
/// ```
|
||||
@@ -796,7 +796,7 @@ where
|
||||
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve before
|
||||
/// sending down its HTML. The app will become interactive once it has fully loaded.
|
||||
///
|
||||
/// This version allows us to pass Axum State/Extension/Extractor or other info from Axum or network
|
||||
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network
|
||||
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
|
||||
/// the data to leptos in a closure. An example is below
|
||||
/// ```
|
||||
@@ -879,8 +879,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Can be used in conjunction with a custom [file_and_error_handler_with_context] to process an Axum [Request](axum::extract::Request) into an Axum [Response](axum::response::Response)
|
||||
pub fn handle_response_inner<IV>(
|
||||
fn handle_response_inner<IV>(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl FnOnce() -> IV + Send + 'static,
|
||||
req: Request<Body>,
|
||||
@@ -1023,7 +1022,7 @@ where
|
||||
/// to route it using [leptos_router], asynchronously rendering an HTML page after all
|
||||
/// `async` resources have loaded.
|
||||
///
|
||||
/// This version allows us to pass Axum State/Extension/Extractor or other info from Axum or network
|
||||
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network
|
||||
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
|
||||
/// the data to leptos in a closure. An example is below
|
||||
/// ```
|
||||
@@ -1090,7 +1089,7 @@ where
|
||||
/// to route it using [leptos_router], asynchronously rendering an HTML page after all
|
||||
/// `async` resources have loaded.
|
||||
///
|
||||
/// This version allows us to pass Axum State/Extension/Extractor or other info from Axum or network
|
||||
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network
|
||||
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
|
||||
/// the data to leptos in a closure. An example is below
|
||||
/// ```
|
||||
|
||||
@@ -9,7 +9,7 @@ rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
futures = { workspace = true, default-features = true }
|
||||
futures = "0.3.31"
|
||||
hydration_context = { workspace = true }
|
||||
leptos = { workspace = true, features = ["nonce"] }
|
||||
leptos_meta = { workspace = true, features = ["ssr"] }
|
||||
|
||||
@@ -15,8 +15,8 @@ any_spawner = { workspace = true, features = [
|
||||
"wasm-bindgen",
|
||||
"futures-executor",
|
||||
] }
|
||||
base64 = { optional = true, workspace = true, default-features = true }
|
||||
cfg-if = { workspace = true, default-features = true }
|
||||
base64 = { version = "0.22.1", optional = true }
|
||||
cfg-if = "1.0"
|
||||
hydration_context = { workspace = true }
|
||||
either_of = { workspace = true }
|
||||
leptos_dom = { workspace = true }
|
||||
@@ -24,38 +24,38 @@ leptos_hot_reload = { workspace = true }
|
||||
leptos_macro = { workspace = true }
|
||||
leptos_server = { workspace = true, features = ["tachys"] }
|
||||
leptos_config = { workspace = true }
|
||||
leptos-spin-macro = { optional = true , workspace = true, default-features = true }
|
||||
leptos-spin-macro = { version = "0.2.0", optional = true }
|
||||
oco_ref = { workspace = true }
|
||||
or_poisoned = { workspace = true }
|
||||
paste = { workspace = true, default-features = true }
|
||||
rand = { optional = true , workspace = true, default-features = true }
|
||||
# NOTE: While not used directly, `getrandom`'s `wasm_js` feature is needed when `rand` is used on WASM to
|
||||
paste = "1.0"
|
||||
rand = { version = "0.8.5", optional = true }
|
||||
# NOTE: While not used directly, `getrandom`'s `js` feature is needed when `rand` is used on WASM to
|
||||
# avoid a compilation error
|
||||
getrandom = { optional = true , workspace = true, default-features = true }
|
||||
getrandom = { version = "0.2", optional = true }
|
||||
reactive_graph = { workspace = true, features = ["serde"] }
|
||||
rustc-hash = { workspace = true, default-features = true }
|
||||
rustc-hash = "2.0"
|
||||
tachys = { workspace = true, features = [
|
||||
"reactive_graph",
|
||||
"reactive_stores",
|
||||
"oco",
|
||||
] }
|
||||
thiserror = { workspace = true, default-features = true }
|
||||
tracing = { optional = true, workspace = true, default-features = true }
|
||||
typed-builder = { workspace = true, default-features = true }
|
||||
typed-builder-macro = { workspace = true, default-features = true }
|
||||
serde = { workspace = true, default-features = true }
|
||||
serde_json = { optional = true, workspace = true, default-features = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
typed-builder = { workspace = true }
|
||||
typed-builder-macro = "0.21.0"
|
||||
serde = "1.0"
|
||||
serde_json = { version = "1.0", optional = true }
|
||||
server_fn = { workspace = true, features = ["form-redirects", "browser"] }
|
||||
web-sys = { features = [
|
||||
web-sys = { version = "0.3.72", features = [
|
||||
"ShadowRoot",
|
||||
"ShadowRootInit",
|
||||
"ShadowRootMode",
|
||||
], workspace = true, default-features = true }
|
||||
wasm-bindgen = { workspace = true, default-features = true }
|
||||
serde_qs = { workspace = true, default-features = true }
|
||||
slotmap = { workspace = true, default-features = true }
|
||||
futures = { workspace = true, default-features = true }
|
||||
send_wrapper = { workspace = true, default-features = true }
|
||||
] }
|
||||
wasm-bindgen = { workspace = true }
|
||||
serde_qs = "0.14.0"
|
||||
slotmap = "1.0"
|
||||
futures = "0.3.31"
|
||||
send_wrapper = "0.6.0"
|
||||
|
||||
[features]
|
||||
hydration = [
|
||||
@@ -64,13 +64,13 @@ hydration = [
|
||||
"hydration_context/browser",
|
||||
"leptos_dom/hydration",
|
||||
]
|
||||
csr = ["leptos_macro/csr", "reactive_graph/effects", "getrandom?/wasm_js"]
|
||||
csr = ["leptos_macro/csr", "reactive_graph/effects", "getrandom?/js"]
|
||||
hydrate = [
|
||||
"leptos_macro/hydrate",
|
||||
"hydration",
|
||||
"tachys/hydrate",
|
||||
"reactive_graph/effects",
|
||||
"getrandom?/wasm_js",
|
||||
"getrandom?/js",
|
||||
]
|
||||
default-tls = ["server_fn/default-tls"]
|
||||
rustls = ["server_fn/rustls"]
|
||||
@@ -101,13 +101,8 @@ trace-component-props = [
|
||||
delegation = ["tachys/delegation"]
|
||||
islands-router = ["tachys/mark_branches"]
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { features = ["rt-multi-thread", "macros"] , workspace = true, default-features = true }
|
||||
tokio-test = { workspace = true, default-features = true }
|
||||
any_spawner = { workspace = true, features = ["futures-executor", "tokio"] }
|
||||
|
||||
[build-dependencies]
|
||||
rustc_version = { workspace = true, default-features = true }
|
||||
rustc_version = "0.4.1"
|
||||
|
||||
# Having an erasure feature rather than normal --cfg erase_components for the proc macro crate is a workaround for this rust issue:
|
||||
# https://github.com/rust-lang/cargo/issues/4423
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
use rustc_version::{version_meta, Channel};
|
||||
|
||||
fn main() {
|
||||
let target = std::env::var("TARGET").unwrap_or_default();
|
||||
|
||||
// Set cfg flags depending on release channel
|
||||
if matches!(version_meta().unwrap().channel, Channel::Nightly) {
|
||||
println!("cargo:rustc-cfg=rustc_nightly");
|
||||
}
|
||||
// Set cfg flag for getrandom wasm_js
|
||||
if target == "wasm32-unknown-unknown" {
|
||||
// Set a custom cfg flag for wasm builds
|
||||
println!("cargo:rustc-cfg=getrandom_backend=\"wasm_js\"");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ type BoxedChildrenFn = Box<dyn Fn() -> AnyView + Send>;
|
||||
///
|
||||
/// Different component types take different types for their `children` prop, some of which cannot
|
||||
/// be directly constructed. Using `ToChildren` allows the component user to pass children without
|
||||
/// explicitly constructing the correct type.
|
||||
/// explicity constructing the correct type.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
|
||||
@@ -255,7 +255,7 @@ where
|
||||
) -> Result<Self, serde_qs::Error>;
|
||||
}
|
||||
|
||||
/// Errors that can arise when converting from an HTML event or form into a Rust data type.
|
||||
/// Errors that can arise when coverting from an HTML event or form into a Rust data type.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum FromFormDataError {
|
||||
/// Could not find a `<form>` connected to the event.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
((root, pkg_path, output_name, wasm_output_name) => {
|
||||
let MOST_RECENT_CHILDREN_CB = [];
|
||||
let MOST_RECENT_CHILDREN_CB;
|
||||
|
||||
function idle(c) {
|
||||
if ("requestIdleCallback" in window) {
|
||||
@@ -22,18 +22,12 @@
|
||||
traverse(child, children);
|
||||
}
|
||||
} else {
|
||||
if (tag === 'leptos-children') {
|
||||
MOST_RECENT_CHILDREN_CB.push(node.$$on_hydrate);
|
||||
for(const child of node.children) {
|
||||
traverse(child);
|
||||
};
|
||||
// un-set the "most recent children"
|
||||
MOST_RECENT_CHILDREN_CB.pop();
|
||||
} else {
|
||||
for(const child of node.children) {
|
||||
traverse(child);
|
||||
};
|
||||
if(tag === 'leptos-children') {
|
||||
MOST_RECENT_CHILDREN_CB = node.$$on_hydrate;
|
||||
}
|
||||
for(const child of node.children) {
|
||||
traverse(child);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,9 +37,8 @@
|
||||
function hydrateIsland(el, id, mod) {
|
||||
const islandFn = mod[id];
|
||||
if (islandFn) {
|
||||
const children_cb = MOST_RECENT_CHILDREN_CB[MOST_RECENT_CHILDREN_CB.length-1];
|
||||
if (children_cb) {
|
||||
children_cb();
|
||||
if (MOST_RECENT_CHILDREN_CB) {
|
||||
MOST_RECENT_CHILDREN_CB();
|
||||
}
|
||||
islandFn(el);
|
||||
} else {
|
||||
@@ -55,7 +48,7 @@
|
||||
idle(() => {
|
||||
import(`${root}/${pkg_path}/${output_name}.js`)
|
||||
.then(mod => {
|
||||
mod.default({module_or_path: `${root}/${pkg_path}/${wasm_output_name}.wasm`}).then(() => {
|
||||
mod.default(`${root}/${pkg_path}/${wasm_output_name}.wasm`).then(() => {
|
||||
mod.hydrate();
|
||||
hydrateIslands(document.body, mod);
|
||||
});
|
||||
|
||||
@@ -17,10 +17,6 @@ window.addEventListener("popstate", async (ev) => {
|
||||
});
|
||||
|
||||
window.addEventListener("submit", async (ev) => {
|
||||
if (ev.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
|
||||
const req = submitToReq(ev);
|
||||
if(!req) {
|
||||
return;
|
||||
@@ -198,15 +194,6 @@ function diffRange(oldDocument, oldRoot, newDocument, newRoot, oldEnd, newEnd) {
|
||||
else if (oldNode.nodeType === Node.TEXT_NODE) {
|
||||
oldNode.textContent = newNode.textContent;
|
||||
}
|
||||
// islands should not be diffed on the client, because we do not want to overwrite client-side state
|
||||
// but their children should be diffed still, because they could contain new server content
|
||||
else if (oldNode.nodeType === Node.ELEMENT_NODE && oldNode.tagName === "LEPTOS-ISLAND") {
|
||||
// TODO: diff the leptos-children
|
||||
|
||||
// skip over leptos-island otherwise
|
||||
oldDocWalker.nextSibling();
|
||||
newDocWalker.nextSibling();
|
||||
}
|
||||
// if it's an element, replace if it's a different tag, or update attributes
|
||||
else if (oldNode.nodeType === Node.ELEMENT_NODE) {
|
||||
diffElement(oldNode, newNode);
|
||||
|
||||
@@ -83,10 +83,6 @@ pub fn HydrationScripts(
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
leptos::logging::error!(
|
||||
"File hashing is active but no hash file was found"
|
||||
);
|
||||
}
|
||||
} else if std::option_env!("LEPTOS_OUTPUT_NAME").is_none() {
|
||||
wasm_file_name.push_str("_bg");
|
||||
|
||||
@@ -7,7 +7,7 @@ ws.onmessage = (ev) => {
|
||||
let found = false;
|
||||
document.querySelectorAll("link").forEach((link) => {
|
||||
if (link.getAttribute('href').includes(msg.css)) {
|
||||
let newHref = '/' + msg.css + '?version=' + Date.now();
|
||||
let newHref = '/' + msg.css + '?version=' + new Date().getMilliseconds();
|
||||
link.setAttribute('href', newHref);
|
||||
found = true;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use base64::{
|
||||
engine::{self, general_purpose},
|
||||
Engine,
|
||||
};
|
||||
use rand::{rng, RngCore};
|
||||
use rand::{thread_rng, RngCore};
|
||||
use std::{fmt::Display, ops::Deref, sync::Arc};
|
||||
use tachys::html::attribute::AttributeValue;
|
||||
|
||||
@@ -171,9 +171,9 @@ const NONCE_ENGINE: engine::GeneralPurpose =
|
||||
impl Nonce {
|
||||
/// Generates a new nonce from 16 bytes (128 bits) of random data.
|
||||
pub fn new() -> Self {
|
||||
let mut rng = rng();
|
||||
let mut thread_rng = thread_rng();
|
||||
let mut bytes = [0; 16];
|
||||
rng.fill_bytes(&mut bytes);
|
||||
thread_rng.fill_bytes(&mut bytes);
|
||||
Nonce(NONCE_ENGINE.encode(bytes).into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use std::sync::Arc;
|
||||
///
|
||||
/// Useful for inserting modals and tooltips outside of a cropping layout.
|
||||
/// If no mount point is given, the portal is inserted in `document.body`;
|
||||
/// it is wrapped in a `<div>` unless `is_svg` is `true` in which case it's wrapped in a `<g>`.
|
||||
/// it is wrapped in a `<div>` unless `is_svg` is `true` in which case it's wrappend in a `<g>`.
|
||||
/// Setting `use_shadow` to `true` places the element in a shadow root to isolate styles.
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
|
||||
#[component]
|
||||
@@ -42,15 +42,11 @@ where
|
||||
let children = children.into_inner();
|
||||
|
||||
Effect::new(move |_| {
|
||||
let container = if is_svg {
|
||||
document()
|
||||
.create_element_ns(Some("http://www.w3.org/2000/svg"), "g")
|
||||
.expect("SVG element creation to work")
|
||||
} else {
|
||||
document()
|
||||
.create_element("div")
|
||||
.expect("HTML element creation to work")
|
||||
};
|
||||
let tag = if is_svg { "g" } else { "div" };
|
||||
|
||||
let container = document()
|
||||
.create_element(tag)
|
||||
.expect("element creation to work");
|
||||
|
||||
let render_root = if use_shadow {
|
||||
container
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
mod imports {
|
||||
pub use any_spawner::Executor;
|
||||
pub use futures::StreamExt;
|
||||
pub use leptos::prelude::*;
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::test]
|
||||
async fn chain_await_resource() {
|
||||
use imports::*;
|
||||
|
||||
_ = Executor::init_tokio();
|
||||
let owner = Owner::new();
|
||||
owner.set();
|
||||
|
||||
let (rs, ws) = signal(0);
|
||||
let source = Resource::new(
|
||||
|| (),
|
||||
move |_| async move {
|
||||
#[cfg(feature = "ssr")]
|
||||
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
|
||||
1
|
||||
},
|
||||
);
|
||||
let consuming = Resource::new(
|
||||
|| (),
|
||||
move |_| async move {
|
||||
let result = source.await;
|
||||
ws.update(|s| *s += 1);
|
||||
result
|
||||
},
|
||||
);
|
||||
let app = view! {
|
||||
<Suspense>{
|
||||
move || {
|
||||
Suspend::new(async move {
|
||||
consuming.await;
|
||||
rs.get()
|
||||
})
|
||||
}
|
||||
}</Suspense>
|
||||
};
|
||||
|
||||
assert_eq!(app.to_html_stream_in_order().collect::<String>().await, "1");
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::test]
|
||||
async fn chain_no_await_resource() {
|
||||
use imports::*;
|
||||
|
||||
_ = Executor::init_tokio();
|
||||
let owner = Owner::new();
|
||||
owner.set();
|
||||
|
||||
let (rs, ws) = signal(0);
|
||||
let source = Resource::new(|| (), move |_| async move { 1 });
|
||||
let consuming = Resource::new(
|
||||
|| (),
|
||||
move |_| async move {
|
||||
let result = source.await;
|
||||
ws.update(|s| *s += 1);
|
||||
result
|
||||
},
|
||||
);
|
||||
let app = view! {
|
||||
<Suspense>{
|
||||
move || {
|
||||
Suspend::new(async move {
|
||||
consuming.await;
|
||||
rs.get()
|
||||
})
|
||||
}
|
||||
}</Suspense>
|
||||
};
|
||||
|
||||
assert_eq!(app.to_html_stream_in_order().collect::<String>().await, "1");
|
||||
}
|
||||
@@ -10,19 +10,19 @@ rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
config = { default-features = false, features = [
|
||||
config = { version = "0.15.8", default-features = false, features = [
|
||||
"toml",
|
||||
"convert-case",
|
||||
] , workspace = true }
|
||||
regex = { workspace = true, default-features = true }
|
||||
serde = { features = ["derive", "rc"] , workspace = true, default-features = true }
|
||||
thiserror = { workspace = true , default-features = true }
|
||||
typed-builder = { workspace = true , default-features = true }
|
||||
] }
|
||||
regex = "1.11"
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
thiserror = { workspace = true }
|
||||
typed-builder = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { features = ["rt", "macros"] , workspace = true, default-features = true }
|
||||
tempfile = { workspace = true, default-features = true }
|
||||
temp-env = { features = ["async_closure"] , workspace = true, default-features = true }
|
||||
tokio = { version = "1.43", features = ["rt", "macros"] }
|
||||
tempfile = "3.14"
|
||||
temp-env = { version = "0.3.6", features = ["async_closure"] }
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--generate-link-to-definition"]
|
||||
|
||||
@@ -12,20 +12,19 @@ edition.workspace = true
|
||||
tachys = { workspace = true }
|
||||
reactive_graph = { workspace = true }
|
||||
or_poisoned = { workspace = true }
|
||||
js-sys = { workspace = true, default-features = true }
|
||||
send_wrapper = { workspace = true, default-features = true }
|
||||
tracing = { optional = true , workspace = true, default-features = true }
|
||||
wasm-bindgen = { workspace = true , default-features = true }
|
||||
serde_json = { optional = true , workspace = true, default-features = true }
|
||||
serde = { optional = true , workspace = true, default-features = true }
|
||||
js-sys = "0.3.74"
|
||||
send_wrapper = "0.6.0"
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
wasm-bindgen = { workspace = true }
|
||||
serde_json = { version = "1.0", optional = true }
|
||||
serde = { version = "1.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
leptos = { path = "../leptos" }
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3.72"
|
||||
features = ["Location"]
|
||||
workspace = true
|
||||
default-features = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
@@ -10,19 +10,19 @@ rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true, default-features = true }
|
||||
serde = { features = ["derive"] , workspace = true, default-features = true }
|
||||
syn = { features = [
|
||||
anyhow = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
syn = { version = "2.0", features = [
|
||||
"full",
|
||||
"parsing",
|
||||
"extra-traits",
|
||||
"visit",
|
||||
"printing",
|
||||
] , workspace = true, default-features = true }
|
||||
quote = { workspace = true, default-features = true }
|
||||
rstml = { workspace = true, default-features = true }
|
||||
proc-macro2 = { features = ["span-locations", "nightly"] , workspace = true, default-features = true }
|
||||
parking_lot = { workspace = true, default-features = true }
|
||||
walkdir = { workspace = true, default-features = true }
|
||||
camino = { workspace = true, default-features = true }
|
||||
indexmap = { workspace = true, default-features = true }
|
||||
] }
|
||||
quote = "1.0"
|
||||
rstml = "0.12.0"
|
||||
proc-macro2 = { version = "1.0", features = ["span-locations", "nightly"] }
|
||||
parking_lot = "0.12.3"
|
||||
walkdir = "2.5"
|
||||
camino = "1.1"
|
||||
indexmap = "2.6"
|
||||
|
||||
@@ -13,39 +13,39 @@ edition.workspace = true
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
attribute-derive = { features = ["syn-full"] , workspace = true, default-features = true }
|
||||
cfg-if = { workspace = true, default-features = true }
|
||||
html-escape = { workspace = true, default-features = true }
|
||||
itertools = { workspace = true , default-features = true }
|
||||
prettyplease = { workspace = true, default-features = true }
|
||||
proc-macro-error2 = { default-features = false , workspace = true }
|
||||
proc-macro2 = { workspace = true, default-features = true }
|
||||
quote = { workspace = true, default-features = true }
|
||||
syn = { features = ["full"] , workspace = true, default-features = true }
|
||||
rstml = { workspace = true, default-features = true }
|
||||
attribute-derive = { version = "0.10.3", features = ["syn-full"] }
|
||||
cfg-if = "1.0"
|
||||
html-escape = "0.2.13"
|
||||
itertools = { workspace = true }
|
||||
prettyplease = "0.2.25"
|
||||
proc-macro-error2 = { version = "2.0", default-features = false }
|
||||
proc-macro2 = "1.0"
|
||||
quote = "1.0"
|
||||
syn = { version = "2.0", features = ["full"] }
|
||||
rstml = "0.12.0"
|
||||
leptos_hot_reload = { workspace = true }
|
||||
server_fn_macro = { workspace = true }
|
||||
convert_case = { workspace = true , default-features = true }
|
||||
uuid = { features = ["v4"] , workspace = true, default-features = true }
|
||||
tracing = { optional = true , workspace = true, default-features = true }
|
||||
convert_case = { workspace = true }
|
||||
uuid = { version = "1.11", features = ["v4"] }
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
log = { workspace = true, default-features = true }
|
||||
typed-builder = { workspace = true, default-features = true }
|
||||
trybuild = { workspace = true , default-features = true }
|
||||
log = "0.4.22"
|
||||
typed-builder = "0.20.0"
|
||||
trybuild = { workspace = true }
|
||||
leptos = { path = "../leptos" }
|
||||
leptos_router = { path = "../router", features = ["ssr"] }
|
||||
server_fn = { path = "../server_fn", features = ["cbor"] }
|
||||
insta = { workspace = true, default-features = true }
|
||||
serde = { workspace = true, default-features = true }
|
||||
insta = "1.41"
|
||||
serde = "1.0"
|
||||
|
||||
[build-dependencies]
|
||||
rustc_version = { workspace = true, default-features = true }
|
||||
rustc_version = "0.4.1"
|
||||
|
||||
[features]
|
||||
csr = []
|
||||
hydrate = []
|
||||
ssr = ["server_fn_macro/ssr"]
|
||||
ssr = ["server_fn_macro/ssr", "leptos/ssr"]
|
||||
nightly = ["server_fn_macro/nightly"]
|
||||
tracing = ["dep:tracing"]
|
||||
islands = []
|
||||
|
||||
@@ -357,13 +357,38 @@ impl ToTokens for Model {
|
||||
|
||||
// add island wrapper if island
|
||||
let component = if is_island {
|
||||
let is_server = cfg!(feature = "ssr");
|
||||
let hydrate_fn_name = hydrate_fn_name.as_ref().unwrap();
|
||||
quote! {
|
||||
::leptos::tachys::html::islands::Island::new(
|
||||
stringify!(#hydrate_fn_name),
|
||||
#component
|
||||
)
|
||||
#island_serialized_props
|
||||
if is_server {
|
||||
quote! {
|
||||
{
|
||||
// if we're on the server, and inside another island already,
|
||||
// *don't* add a <leptos-island> (this will hydrate them both separately)
|
||||
if ::leptos::context::use_context::<::leptos::reactive::owner::IsHydrating>()
|
||||
.map(|h| h.0)
|
||||
.unwrap_or(false) {
|
||||
::leptos::either::Either::Left(
|
||||
#component
|
||||
)
|
||||
} else {
|
||||
::leptos::either::Either::Right(
|
||||
::leptos::tachys::html::islands::Island::new(
|
||||
stringify!(#hydrate_fn_name),
|
||||
#component
|
||||
)
|
||||
#island_serialized_props
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
::leptos::tachys::html::islands::Island::new(
|
||||
stringify!(#hydrate_fn_name),
|
||||
#component
|
||||
)
|
||||
#island_serialized_props
|
||||
}
|
||||
}
|
||||
} else {
|
||||
component
|
||||
@@ -634,13 +659,7 @@ impl Parse for DummyModel {
|
||||
let mut attrs = input.call(Attribute::parse_outer)?;
|
||||
// Drop unknown attributes like #[deprecated]
|
||||
drain_filter(&mut attrs, |attr| {
|
||||
let path = attr.path();
|
||||
!(path.is_ident("doc")
|
||||
|| path.is_ident("allow")
|
||||
|| path.is_ident("expect")
|
||||
|| path.is_ident("warn")
|
||||
|| path.is_ident("deny")
|
||||
|| path.is_ident("forbid"))
|
||||
!(attr.path().is_ident("doc") || attr.path().is_ident("allow"))
|
||||
});
|
||||
|
||||
let vis: Visibility = input.parse()?;
|
||||
@@ -929,20 +948,13 @@ impl UnknownAttrs {
|
||||
let attrs = attrs
|
||||
.iter()
|
||||
.filter_map(|attr| {
|
||||
let path = attr.path();
|
||||
|
||||
if path.is_ident("doc") {
|
||||
if attr.path().is_ident("doc") {
|
||||
if let Meta::NameValue(_) = &attr.meta {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
if path.is_ident("allow")
|
||||
|| path.is_ident("expect")
|
||||
|| path.is_ident("warn")
|
||||
|| path.is_ident("deny")
|
||||
|| path.is_ident("forbid")
|
||||
{
|
||||
if attr.path().is_ident("allow") {
|
||||
return None;
|
||||
}
|
||||
|
||||
|
||||
@@ -409,7 +409,6 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
|
||||
/// generate documentation for the component.
|
||||
///
|
||||
/// Here’s how you would define and use a simple Leptos component which can accept custom properties for a name and age:
|
||||
///
|
||||
/// ```rust
|
||||
/// # use leptos::prelude::*;
|
||||
/// use std::time::Duration;
|
||||
@@ -447,7 +446,6 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
|
||||
/// ```
|
||||
///
|
||||
/// Here are some important details about how Leptos components work within the framework:
|
||||
///
|
||||
/// * **The component function only runs once.** Your component function is not a “render” function
|
||||
/// that re-runs whenever changes happen in the state. It’s a “setup” function that runs once to
|
||||
/// create the user interface, and sets up a reactive system to update it. This means it’s okay
|
||||
@@ -460,6 +458,7 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos::prelude::*;
|
||||
///
|
||||
/// // PascalCase: Generated component will be called MyComponent
|
||||
/// #[component]
|
||||
/// fn MyComponent() -> impl IntoView {}
|
||||
@@ -501,10 +500,8 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
|
||||
/// ```
|
||||
///
|
||||
/// ## Customizing Properties
|
||||
///
|
||||
/// You can use the `#[prop]` attribute on individual component properties (function arguments) to
|
||||
/// customize the types that component property can receive. You can use the following attributes:
|
||||
///
|
||||
/// * `#[prop(into)]`: This will call `.into()` on any value passed into the component prop. (For example,
|
||||
/// you could apply `#[prop(into)]` to a prop that takes
|
||||
/// [Signal](https://docs.rs/leptos/latest/leptos/struct.Signal.html), which would
|
||||
@@ -517,11 +514,6 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
|
||||
/// * `#[prop(optional_no_strip)]`: The same as `optional`, but requires values to be passed as `None` or
|
||||
/// `Some(T)` explicitly. This means that the optional property can be omitted (and be `None`), or explicitly
|
||||
/// specified as either `None` or `Some(T)`.
|
||||
/// * `#[prop(default = <expr>)]`: Optional property that specifies a default value, which is used when the
|
||||
/// property is not specified.
|
||||
/// * `#[prop(name = "new_name")]`: Specifiy a different name for the property. Can be used to destructure
|
||||
/// fields in component function parameters (see example below).
|
||||
///
|
||||
/// ```rust
|
||||
/// # use leptos::prelude::*;
|
||||
///
|
||||
@@ -530,8 +522,6 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
|
||||
/// #[prop(into)] name: String,
|
||||
/// #[prop(optional)] optional_value: Option<i32>,
|
||||
/// #[prop(optional_no_strip)] optional_no_strip: Option<i32>,
|
||||
/// #[prop(default = 7)] optional_default: i32,
|
||||
/// #[prop(name = "data")] UserInfo { email, user_id }: UserInfo,
|
||||
/// ) -> impl IntoView {
|
||||
/// // whatever UI you need
|
||||
/// }
|
||||
@@ -540,24 +530,16 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
|
||||
/// pub fn App() -> impl IntoView {
|
||||
/// view! {
|
||||
/// <MyComponent
|
||||
/// name="Greg" // automatically converted to String with `.into()`
|
||||
/// optional_value=42 // received as `Some(42)`
|
||||
/// optional_no_strip=Some(42) // received as `Some(42)`
|
||||
/// optional_default=42 // received as `42`
|
||||
/// data=UserInfo {email: "foo", user_id: "bar" }
|
||||
/// name="Greg" // automatically converted to String with `.into()`
|
||||
/// optional_value=42 // received as `Some(42)`
|
||||
/// optional_no_strip=Some(42) // received as `Some(42)`
|
||||
/// />
|
||||
/// <MyComponent
|
||||
/// name="Bob" // automatically converted to String with `.into()`
|
||||
/// data=UserInfo {email: "foo", user_id: "bar" }
|
||||
/// // optional values can be omitted
|
||||
/// // optional values can both be omitted, and received as `None`
|
||||
/// />
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// pub struct UserInfo {
|
||||
/// pub email: &'static str,
|
||||
/// pub user_id: &'static str,
|
||||
/// }
|
||||
/// ```
|
||||
#[proc_macro_error2::proc_macro_error]
|
||||
#[proc_macro_attribute]
|
||||
|
||||
@@ -431,9 +431,7 @@ fn element_children_to_tokens(
|
||||
} else if cfg!(feature = "__internal_erase_components") {
|
||||
Some(quote! {
|
||||
.child(
|
||||
::leptos::tachys::view::iterators::StaticVec::from(vec![#(
|
||||
::leptos::prelude::IntoMaybeErased::into_maybe_erased(#children)
|
||||
),*])
|
||||
leptos::tachys::view::iterators::StaticVec::from(vec![#(#children.into_maybe_erased()),*])
|
||||
)
|
||||
})
|
||||
} else if children.len() > 16 {
|
||||
@@ -483,9 +481,7 @@ fn fragment_to_tokens(
|
||||
children.into_iter().next()
|
||||
} else if cfg!(feature = "__internal_erase_components") {
|
||||
Some(quote! {
|
||||
::leptos::tachys::view::iterators::StaticVec::from(vec![#(
|
||||
::leptos::prelude::IntoMaybeErased::into_maybe_erased(#children)
|
||||
),*])
|
||||
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
|
||||
|
||||
@@ -119,45 +119,3 @@ fn returns_static_lifetime() {
|
||||
WithLifetime(WithLifetimeProps::builder().data(&val).build())
|
||||
}
|
||||
}
|
||||
|
||||
// an attempt to catch unhygienic macros regression
|
||||
mod macro_hygiene {
|
||||
// To ensure no relative module path to leptos inside macros.
|
||||
mod leptos {}
|
||||
|
||||
// doing this separately to below due to this being the smallest
|
||||
// unit with the lowest import surface.
|
||||
#[test]
|
||||
fn view() {
|
||||
use ::leptos::IntoView;
|
||||
use ::leptos_macro::{component, view};
|
||||
|
||||
#[component]
|
||||
fn Component() -> impl IntoView {
|
||||
view! {
|
||||
{()}
|
||||
{()}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// may extend this test with other items as necessary.
|
||||
#[test]
|
||||
fn view_into_any() {
|
||||
use ::leptos::{
|
||||
prelude::{ElementChild, IntoAny},
|
||||
IntoView,
|
||||
};
|
||||
use ::leptos_macro::{component, view};
|
||||
|
||||
#[component]
|
||||
fn Component() -> impl IntoView {
|
||||
view! {
|
||||
<div>
|
||||
{().into_any()}
|
||||
{()}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,24 +10,24 @@ rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
base64 = { workspace = true, default-features = true }
|
||||
codee = { features = ["json_serde"] , workspace = true, default-features = true }
|
||||
base64 = "0.22.1"
|
||||
codee = { version = "0.3.0", features = ["json_serde"] }
|
||||
hydration_context = { workspace = true }
|
||||
reactive_graph = { workspace = true, features = ["hydration"] }
|
||||
server_fn = { workspace = true }
|
||||
tracing = { optional = true , workspace = true, default-features = true }
|
||||
futures = { workspace = true, default-features = true }
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
futures = "0.3.31"
|
||||
|
||||
any_spawner = { workspace = true }
|
||||
or_poisoned = { workspace = true }
|
||||
tachys = { workspace = true, optional = true, features = ["reactive_graph"] }
|
||||
send_wrapper = { workspace = true, default-features = true }
|
||||
send_wrapper = "0.6"
|
||||
|
||||
# serialization formats
|
||||
serde = { workspace = true, default-features = true }
|
||||
js-sys = { optional = true , workspace = true, default-features = true }
|
||||
wasm-bindgen = { workspace = true, optional = true , default-features = true }
|
||||
serde_json = { workspace = true , default-features = true }
|
||||
serde = { version = "1.0" }
|
||||
js-sys = { version = "0.3.74", optional = true }
|
||||
wasm-bindgen = { workspace = true, optional = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[features]
|
||||
ssr = []
|
||||
|
||||
@@ -14,13 +14,11 @@ use reactive_graph::{
|
||||
ArcRwSignal, RwSignal,
|
||||
},
|
||||
traits::{
|
||||
DefinedAt, IsDisposed, Notify, ReadUntracked, Track, UntrackableGuard,
|
||||
Update, With, Write,
|
||||
DefinedAt, IsDisposed, ReadUntracked, Track, Update, With, Write,
|
||||
},
|
||||
};
|
||||
use std::{
|
||||
future::{pending, Future, IntoFuture},
|
||||
ops::DerefMut,
|
||||
panic::Location,
|
||||
};
|
||||
|
||||
@@ -64,7 +62,7 @@ impl<T> ArcLocalResource<T> {
|
||||
pending().await
|
||||
} else {
|
||||
// LocalResources that are immediately available can cause a hydration error,
|
||||
// because the future *looks* like it is already ready (and therefore would
|
||||
// because the future *looks* like it is alredy ready (and therefore would
|
||||
// already have been rendered to html on the server), but in fact was ignored
|
||||
// on the server. the simplest way to avoid this is to ensure that we always
|
||||
// wait a tick before resolving any value for a localresource.
|
||||
@@ -159,32 +157,6 @@ impl<T> DefinedAt for ArcLocalResource<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Notify for ArcLocalResource<T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
fn notify(&self) {
|
||||
self.data.notify()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Write for ArcLocalResource<T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
type Value = Option<T>;
|
||||
|
||||
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
|
||||
self.data.try_write()
|
||||
}
|
||||
|
||||
fn try_write_untracked(
|
||||
&self,
|
||||
) -> Option<impl DerefMut<Target = Self::Value>> {
|
||||
self.data.try_write_untracked()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ReadUntracked for ArcLocalResource<T>
|
||||
where
|
||||
T: 'static,
|
||||
@@ -298,7 +270,7 @@ impl<T> LocalResource<T> {
|
||||
pending().await
|
||||
} else {
|
||||
// LocalResources that are immediately available can cause a hydration error,
|
||||
// because the future *looks* like it is already ready (and therefore would
|
||||
// because the future *looks* like it is alredy ready (and therefore would
|
||||
// already have been rendered to html on the server), but in fact was ignored
|
||||
// on the server. the simplest way to avoid this is to ensure that we always
|
||||
// wait a tick before resolving any value for a localresource.
|
||||
@@ -328,34 +300,6 @@ impl<T> LocalResource<T> {
|
||||
pub fn refetch(&self) {
|
||||
self.refetch.try_update(|n| *n += 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> LocalResource<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 LocalResource<T>
|
||||
@@ -392,32 +336,6 @@ impl<T> DefinedAt for LocalResource<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Notify for LocalResource<T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
fn notify(&self) {
|
||||
self.data.notify()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Write for LocalResource<T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
type Value = Option<T>;
|
||||
|
||||
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
|
||||
self.data.try_write()
|
||||
}
|
||||
|
||||
fn try_write_untracked(
|
||||
&self,
|
||||
) -> Option<impl DerefMut<Target = Self::Value>> {
|
||||
self.data.try_write_untracked()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ReadUntracked for LocalResource<T>
|
||||
where
|
||||
T: 'static,
|
||||
|
||||
@@ -26,7 +26,7 @@ use reactive_graph::{
|
||||
};
|
||||
use std::{
|
||||
future::{pending, IntoFuture},
|
||||
ops::{Deref, DerefMut},
|
||||
ops::Deref,
|
||||
panic::Location,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
@@ -162,32 +162,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> Notify for ArcResource<T, Ser>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
fn notify(&self) {
|
||||
self.data.notify()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> Write for ArcResource<T, Ser>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
type Value = Option<T>;
|
||||
|
||||
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
|
||||
self.data.try_write()
|
||||
}
|
||||
|
||||
fn try_write_untracked(
|
||||
&self,
|
||||
) -> Option<impl DerefMut<Target = Self::Value>> {
|
||||
self.data.try_write_untracked()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> ReadUntracked for ArcResource<T, Ser>
|
||||
where
|
||||
T: 'static,
|
||||
@@ -868,32 +842,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> Notify for Resource<T, Ser>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
fn notify(&self) {
|
||||
self.data.notify()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> Write for Resource<T, Ser>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
type Value = Option<T>;
|
||||
|
||||
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
|
||||
self.data.try_write()
|
||||
}
|
||||
|
||||
fn try_write_untracked(
|
||||
&self,
|
||||
) -> Option<impl DerefMut<Target = Self::Value>> {
|
||||
self.data.try_write_untracked()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> ReadUntracked for Resource<T, Ser>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_meta"
|
||||
version = "0.8.2"
|
||||
version = "0.8.0-rc3"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
@@ -10,18 +10,17 @@ edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { workspace = true }
|
||||
once_cell = { workspace = true, default-features = true }
|
||||
once_cell = "1.20"
|
||||
or_poisoned = { workspace = true }
|
||||
indexmap = { workspace = true, default-features = true }
|
||||
send_wrapper = { workspace = true, default-features = true }
|
||||
tracing = { optional = true , workspace = true, default-features = true }
|
||||
wasm-bindgen = { workspace = true , default-features = true }
|
||||
futures = { workspace = true, default-features = true }
|
||||
indexmap = "2.6"
|
||||
send_wrapper = "0.6.0"
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
wasm-bindgen = { workspace = true }
|
||||
futures = "0.3.31"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3.72"
|
||||
features = ["HtmlLinkElement", "HtmlMetaElement", "HtmlTitleElement"]
|
||||
workspace = true
|
||||
default-features = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
@@ -216,13 +216,6 @@ impl ServerMetaContextOutput {
|
||||
self,
|
||||
mut stream: impl Stream<Item = String> + Send + Unpin,
|
||||
) -> impl Stream<Item = String> + Send {
|
||||
// if the first chunk consists of a synchronously-available Suspend,
|
||||
// inject_meta_context can accidentally run a tick before it, but the Suspend
|
||||
// when both are available. waiting a tick before awaiting the first chunk
|
||||
// in the Stream ensures that this always runs after that first chunk
|
||||
// see https://github.com/leptos-rs/leptos/issues/3976 for the original issue
|
||||
leptos::task::tick().await;
|
||||
|
||||
// wait for the first chunk of the stream, to ensure our components hve run
|
||||
let mut first_chunk = stream.next().await.unwrap_or_default();
|
||||
|
||||
@@ -249,22 +242,23 @@ impl ServerMetaContextOutput {
|
||||
let head_loc = first_chunk
|
||||
.find("</head>")
|
||||
.expect("you are using leptos_meta without a </head> tag");
|
||||
let marker_loc = first_chunk
|
||||
.find("<!--HEAD-->")
|
||||
.map(|pos| pos + "<!--HEAD-->".len())
|
||||
.unwrap_or_else(|| {
|
||||
let marker_loc =
|
||||
first_chunk.find("<!--HEAD-->").unwrap_or_else(|| {
|
||||
first_chunk.find("</head>").unwrap_or(head_loc)
|
||||
});
|
||||
let (before_marker, after_marker) =
|
||||
first_chunk.split_at_mut(marker_loc);
|
||||
let (before_head_close, after_head) =
|
||||
after_marker.split_at_mut(head_loc - marker_loc);
|
||||
buf.push_str(before_marker);
|
||||
buf.push_str(&meta_buf);
|
||||
if let Some(title) = title {
|
||||
buf.push_str("<title>");
|
||||
buf.push_str(&title);
|
||||
buf.push_str("</title>");
|
||||
}
|
||||
buf.push_str(after_marker);
|
||||
buf.push_str(before_head_close);
|
||||
buf.push_str(&meta_buf);
|
||||
buf.push_str(after_head);
|
||||
buf
|
||||
};
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true, default-features = true }
|
||||
thiserror = { workspace = true , default-features = true }
|
||||
serde = "1.0"
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = { workspace = true , default-features = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -19,9 +19,9 @@ leptos_router = { path = "../../router" }
|
||||
log = "0.4.0"
|
||||
simple_logger = "5.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
axum = { version = "0.8.0", optional = true, features = ["macros"] }
|
||||
tower = { version = "0.5.0", optional = true }
|
||||
tower-http = { version = "0.6.0", features = ["fs"], optional = true }
|
||||
axum = { version = "0.7.0", optional = true, features = ["macros"] }
|
||||
tower = { version = "0.4.0", optional = true }
|
||||
tower-http = { version = "0.5.0", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.0", features = ["full"], optional = true }
|
||||
http = { version = "1.0" }
|
||||
sqlx = { version = "0.8.0", features = [
|
||||
@@ -30,13 +30,10 @@ sqlx = { version = "0.8.0", features = [
|
||||
], optional = true }
|
||||
thiserror = "1.0"
|
||||
wasm-bindgen = "0.2.0"
|
||||
axum_session_auth = { version = "0.16.0", features = [], optional = true }
|
||||
axum_session = { version = "0.16.0", features = [], optional = true }
|
||||
axum_session_sqlx = { version = "0.5.0", features = [
|
||||
"sqlite",
|
||||
"tls-rustls",
|
||||
], optional = true }
|
||||
bcrypt = { version = "0.17.0", optional = true }
|
||||
axum_session_auth = { version = "0.14.0", features = [], optional = true }
|
||||
axum_session = { version = "0.14.0", features = [], optional = true }
|
||||
axum_session_sqlx = { version = "0.3.0", features = [ "sqlite", "tls-rustls"], optional = true }
|
||||
bcrypt = { version = "0.15.0", optional = true }
|
||||
async-trait = { version = "0.1.0", optional = true }
|
||||
|
||||
[features]
|
||||
|
||||
@@ -185,7 +185,7 @@ pub async fn foo() -> Result<String, ServerFnError> {
|
||||
pub async fn get_user() -> Result<Option<User>, ServerFnError> {
|
||||
use crate::todo::ssr::auth;
|
||||
|
||||
let auth = auth().await?;
|
||||
let auth = auth()?;
|
||||
|
||||
Ok(auth.current_user)
|
||||
}
|
||||
@@ -199,7 +199,7 @@ pub async fn login(
|
||||
use self::ssr::*;
|
||||
|
||||
let pool = pool()?;
|
||||
let auth = auth().await?;
|
||||
let auth = auth()?;
|
||||
|
||||
let (user, UserPasshash(expected_passhash)) =
|
||||
User::get_from_username_with_passhash(username, &pool)
|
||||
@@ -229,7 +229,7 @@ pub async fn signup(
|
||||
use self::ssr::*;
|
||||
|
||||
let pool = pool()?;
|
||||
let auth = auth().await?;
|
||||
let auth = auth()?;
|
||||
|
||||
if password != password_confirmation {
|
||||
return Err(ServerFnError::ServerError(
|
||||
@@ -264,7 +264,7 @@ pub async fn signup(
|
||||
pub async fn logout() -> Result<(), ServerFnError> {
|
||||
use self::ssr::*;
|
||||
|
||||
let auth = auth().await?;
|
||||
let auth = auth()?;
|
||||
|
||||
auth.logout_user();
|
||||
leptos_axum::redirect("/");
|
||||
|
||||
@@ -1,12 +1,62 @@
|
||||
use axum::Router;
|
||||
use axum::{
|
||||
body::Body as AxumBody,
|
||||
extract::{Path, State},
|
||||
http::Request,
|
||||
response::{IntoResponse, Response},
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use axum_session::{SessionConfig, SessionLayer, SessionStore};
|
||||
use axum_session_auth::{AuthConfig, AuthSessionLayer};
|
||||
use axum_session_sqlx::SessionSqlitePool;
|
||||
use leptos::{config::get_configuration, logging::log};
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use session_auth_axum::{auth::User, state::AppState, todo::*};
|
||||
use leptos::{
|
||||
config::get_configuration, logging::log, prelude::provide_context,
|
||||
};
|
||||
use leptos_axum::{
|
||||
generate_route_list, handle_server_fns_with_context, LeptosRoutes,
|
||||
};
|
||||
use session_auth_axum::{
|
||||
auth::{ssr::AuthSession, User},
|
||||
state::AppState,
|
||||
todo::*,
|
||||
};
|
||||
use sqlx::{sqlite::SqlitePoolOptions, SqlitePool};
|
||||
|
||||
async fn server_fn_handler(
|
||||
State(app_state): State<AppState>,
|
||||
auth_session: AuthSession,
|
||||
path: Path<String>,
|
||||
request: Request<AxumBody>,
|
||||
) -> impl IntoResponse {
|
||||
log!("{:?}", path);
|
||||
|
||||
handle_server_fns_with_context(
|
||||
move || {
|
||||
provide_context(auth_session.clone());
|
||||
provide_context(app_state.pool.clone());
|
||||
},
|
||||
request,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn leptos_routes_handler(
|
||||
auth_session: AuthSession,
|
||||
state: State<AppState>,
|
||||
req: Request<AxumBody>,
|
||||
) -> Response {
|
||||
let State(app_state) = state.clone();
|
||||
let handler = leptos_axum::render_route_with_context(
|
||||
app_state.routes.clone(),
|
||||
move || {
|
||||
provide_context(auth_session.clone());
|
||||
provide_context(app_state.pool.clone());
|
||||
},
|
||||
move || shell(app_state.leptos_options.clone()),
|
||||
);
|
||||
handler(state, req).await.into_response()
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
simple_logger::init_with_level(log::Level::Info)
|
||||
@@ -32,6 +82,18 @@ async fn main() {
|
||||
eprintln!("{e:?}");
|
||||
}
|
||||
|
||||
// Explicit server function registration is no longer required
|
||||
// on the main branch. On 0.3.0 and earlier, uncomment the lines
|
||||
// below to register the server functions.
|
||||
// _ = GetTodos::register();
|
||||
// _ = AddTodo::register();
|
||||
// _ = DeleteTodo::register();
|
||||
// _ = Login::register();
|
||||
// _ = Logout::register();
|
||||
// _ = Signup::register();
|
||||
// _ = GetUser::register();
|
||||
// _ = Foo::register();
|
||||
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars
|
||||
let conf = get_configuration(None).unwrap();
|
||||
let leptos_options = conf.leptos_options;
|
||||
@@ -46,10 +108,11 @@ async fn main() {
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.leptos_routes(&app_state, routes, {
|
||||
let options = app_state.leptos_options.clone();
|
||||
move || shell(options.clone())
|
||||
})
|
||||
.route(
|
||||
"/api/*fn_name",
|
||||
get(server_fn_handler).post(server_fn_handler),
|
||||
)
|
||||
.leptos_routes_with_handler(routes, get(leptos_routes_handler))
|
||||
.fallback(leptos_axum::file_and_error_handler::<AppState, _>(shell))
|
||||
.layer(
|
||||
AuthSessionLayer::<User, i64, SessionSqlitePool, SqlitePool>::new(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{auth::*, error_template::ErrorTemplate};
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::{components::*, path};
|
||||
use leptos_router::{components::*, *};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -16,21 +16,19 @@ pub struct Todo {
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod ssr {
|
||||
use super::Todo;
|
||||
use crate::{
|
||||
auth::{ssr::AuthSession, User},
|
||||
state::AppState,
|
||||
};
|
||||
use crate::auth::{ssr::AuthSession, User};
|
||||
use leptos::prelude::*;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
pub fn pool() -> Result<SqlitePool, ServerFnError> {
|
||||
with_context::<AppState, _>(|state| state.pool.clone())
|
||||
use_context::<SqlitePool>()
|
||||
.ok_or_else(|| ServerFnError::ServerError("Pool missing.".into()))
|
||||
}
|
||||
|
||||
pub async fn auth() -> Result<AuthSession, ServerFnError> {
|
||||
let auth = leptos_axum::extract().await?;
|
||||
Ok(auth)
|
||||
pub fn auth() -> Result<AuthSession, ServerFnError> {
|
||||
use_context::<AuthSession>().ok_or_else(|| {
|
||||
ServerFnError::ServerError("Auth session missing.".into())
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Clone)]
|
||||
@@ -167,7 +165,7 @@ pub fn TodoApp() -> impl IntoView {
|
||||
", "
|
||||
<A href="/login">"Login"</A>
|
||||
", "
|
||||
<span>{format!("Login error: {e}")}</span>
|
||||
<span>{format!("Login error: {}", e)}</span>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
|
||||
1
projects/tauri-from-scratch/.gitignore
vendored
1
projects/tauri-from-scratch/.gitignore
vendored
@@ -1,2 +1 @@
|
||||
/target
|
||||
gen/
|
||||
|
||||
@@ -5,3 +5,4 @@ members = ["src-tauri", "src-orig"]
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
|
||||
|
||||
@@ -1,27 +1,20 @@
|
||||
# Tauri from scratch
|
||||
|
||||
This is a guide on how to build a leptos tauri project from scratch without using a template.
|
||||
|
||||
<br><br>
|
||||
First
|
||||
|
||||
```sh
|
||||
cargo new leptos_tauri_from_scratch
|
||||
```
|
||||
|
||||
Then, make our two separate project folders. We need one for our actual app, _src-orig/_ and the other is required when using `cargo tauri`
|
||||
|
||||
Then, make our two separate project folders. We need one for our actual app, 'src-orig' and the other is required when using `cargo tauri`
|
||||
```sh
|
||||
mkdir src-orig && mkdir src-tauri
|
||||
```
|
||||
|
||||
Delete the original src folder.
|
||||
|
||||
```sh
|
||||
rm -r src
|
||||
```
|
||||
|
||||
Rewrite the `Cargo.toml` file in our crate root to the following.
|
||||
|
||||
```toml
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
@@ -31,13 +24,10 @@ members = ["src-tauri", "src-orig"]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
```
|
||||
|
||||
We'll list our workspace members. `codegen-units = 1` and `lto = true` are good things to have for our eventual release, they make the wasm file smaller.
|
||||
|
||||
We'll use resolver two because we're using a modern version of Rust. We'll list our workspace members. `codegen-units = 1` and `lto = true` are good things to have for our eventual release, they make the wasm file smaller.
|
||||
<br><br>
|
||||
What we're going to do is use `cargo leptos` for building our SSR server and we'll call trunk from `cargo tauri` for building our CSR client that we bundle into our different apps.
|
||||
|
||||
Let's add a `Trunk.toml` file.
|
||||
|
||||
```toml
|
||||
[build]
|
||||
target = "./src-orig/index.html"
|
||||
@@ -47,7 +37,7 @@ ignore = ["./src-tauri"]
|
||||
```
|
||||
|
||||
The target of `index.html` is what trunk uses to build the wasm and js files that we'll need for the bundling process when we call `cargo tauri build`. We'll get the resulting files in a `src-orig/dist` folder.
|
||||
|
||||
<br>
|
||||
Create the `index.html` file
|
||||
|
||||
```sh
|
||||
@@ -55,95 +45,47 @@ touch src-orig/index.html
|
||||
```
|
||||
|
||||
Let's fill it with
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link
|
||||
data-trunk
|
||||
rel="rust"
|
||||
data-wasm-opt="z"
|
||||
data-bin="leptos_tauri_from_scratch_bin"
|
||||
/>
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
</head>
|
||||
<body></body>
|
||||
<head>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z" data-bin="leptos_tauri_from_scratch_bin"/>
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
```
|
||||
|
||||
This line
|
||||
|
||||
```html
|
||||
<link
|
||||
data-trunk
|
||||
rel="rust"
|
||||
data-wasm-opt="z"
|
||||
data-bin="leptos_tauri_from_scratch_bin"
|
||||
/>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z" data-bin="leptos_tauri_from_scratch_bin"/>
|
||||
```
|
||||
|
||||
Tells trunk we want to compile our wasm to be small with `opt="z"` and that our binary will be named `"leptos_tauri_from_scratch_bin"`.
|
||||
|
||||
We need to specify that our binary will be a different name than our project name because we are also going to get a wasm file from our library and if we don't use different names then `cargo tauri` will get confused.
|
||||
|
||||
Tells trunk we want to compile our wasm to be small with `opt="z"` and that our binary will be named `"leptos_tauri_from_scratch_bin"`. <br>
|
||||
We need to specify that our binary will be a different name then our project name because we are also going to get a wasm file from our library and if we don't use different names then `cargo tauri` will get confused. <br>
|
||||
More specifically two wasm artifacts will be generated, one for the lib and the other for the binary and it won't know which to use.
|
||||
|
||||
<br><br>
|
||||
Create a favicon that we referenced.
|
||||
|
||||
```sh
|
||||
mkdir public
|
||||
curl https://raw.githubusercontent.com/leptos-rs/leptos/main/examples/counter/public/favicon.ico > public/favicon.ico
|
||||
```
|
||||
|
||||
mkdir public && curl https://raw.githubusercontent.com/leptos-rs/leptos/main/examples/animated_show/public/favicon.ico > public/favicon.ico
|
||||
```
|
||||
<br><br>
|
||||
Let's create a tauri configuration file.
|
||||
|
||||
```sh
|
||||
touch src-tauri/taur.conf.json
|
||||
```
|
||||
|
||||
And drop this in there
|
||||
|
||||
```json
|
||||
{
|
||||
"identifier": "leptos.chat.app",
|
||||
"productName": "leptos_tauri_from_scratch",
|
||||
"version": "0.1.0",
|
||||
"build": {
|
||||
"beforeDevCommand": "",
|
||||
"beforeBuildCommand": "trunk build --no-default-features -v --features \"csr\"",
|
||||
"devUrl": "http://127.0.0.1:3000",
|
||||
"frontendDist": "../dist"
|
||||
"devPath": "http://127.0.0.1:3000",
|
||||
"distDir": "../src-orig/dist"
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"category": "DeveloperTool",
|
||||
"copyright": "",
|
||||
"externalBin": [],
|
||||
"icon": ["icons/icon.png"],
|
||||
"longDescription": "",
|
||||
"macOS": {
|
||||
"entitlements": null,
|
||||
"exceptionDomain": "",
|
||||
"frameworks": [],
|
||||
"providerShortName": null,
|
||||
"signingIdentity": null
|
||||
},
|
||||
"resources": [],
|
||||
"shortDescription": "",
|
||||
"targets": "all",
|
||||
"windows": {
|
||||
"certificateThumbprint": null,
|
||||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": ""
|
||||
}
|
||||
"package": {
|
||||
"productName": "leptos_tauri_from_scratch",
|
||||
"version": "0.1.0"
|
||||
},
|
||||
"app": {
|
||||
"security": {
|
||||
"csp": null
|
||||
},
|
||||
"tauri": {
|
||||
"windows": [
|
||||
{
|
||||
"fullscreen": false,
|
||||
@@ -152,53 +94,69 @@ And drop this in there
|
||||
"title": "LeptosChatApp",
|
||||
"width": 1200
|
||||
}
|
||||
]
|
||||
],
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"category": "DeveloperTool",
|
||||
"copyright": "",
|
||||
"deb": {
|
||||
"depends": []
|
||||
},
|
||||
"externalBin": [],
|
||||
"icon": [],
|
||||
"identifier": "leptos.chat.app",
|
||||
"longDescription": "",
|
||||
"macOS": {
|
||||
"entitlements": null,
|
||||
"exceptionDomain": "",
|
||||
"frameworks": [],
|
||||
"providerShortName": null,
|
||||
"signingIdentity": null
|
||||
},
|
||||
"resources": [],
|
||||
"shortDescription": "",
|
||||
"targets": "all",
|
||||
"windows": {
|
||||
"certificateThumbprint": null,
|
||||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": ""
|
||||
}
|
||||
},
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can basically ignore all of this except for
|
||||
|
||||
```json
|
||||
"build": {
|
||||
"beforeDevCommand": "",
|
||||
"beforeBuildCommand": "trunk build --no-default-features -v --features \"csr\"",
|
||||
"devUrl": "http://127.0.0.1:3000",
|
||||
"frontendDist": "../dist"
|
||||
"devPath": "http://127.0.0.1:3000",
|
||||
"distDir": "../src-orig/dist"
|
||||
},
|
||||
```
|
||||
|
||||
Let's look at
|
||||
|
||||
Let's look at
|
||||
```json
|
||||
"beforeBuildCommand": "trunk build --no-default-features -v --features \"csr\"",
|
||||
```
|
||||
|
||||
When we `cargo tauri build` this will run before hand. Trunk will run it's build process, using the index.html file in the src-orig that we specified in `Trunk.toml`.
|
||||
|
||||
We'll build a binary using only the CSR feature. This is important.
|
||||
|
||||
We are going to build an SSR app, and serve it over the internet but we are also going to build a tauri client for desktop and mobile using CSR.
|
||||
|
||||
It's going to make network requests to our server that is servering our app to browsers using SSR.
|
||||
|
||||
This is the best of both worlds, we get the SEO of SSR and other advantages while being able to use CSR to build our app for other platforms.
|
||||
|
||||
```json
|
||||
"devUrl": "http://127.0.0.1:3000",
|
||||
"frontendDist": "../dist"
|
||||
When we `cargo tauri build` this will run before hand. Trunk will run it's build process, using the index.html file in the src-orig that we specified in `Trunk.toml` <br>
|
||||
We'll build a binary using only the CSR feature. This is important. <br>
|
||||
We are going to build an SSR app, and serve it over the internet but we are also going to build a tauri client for desktop and mobile using CSR.<br>
|
||||
It's going to make network requests to our server that is servering our app to browsers using SSR.<br>
|
||||
This is the best of both worlds, we get the SEO of SSR and other advantages while being able to use CSR to build our app for other platforms.
|
||||
```
|
||||
|
||||
Check <https://tauri.app/v1/api/config/#buildconfig> for what these do, but our before build command `trunk build` will build into a folder `src-orig/dist` which we reference here.
|
||||
|
||||
"devPath": "http://127.0.0.1:3000",
|
||||
"distDir": "../src-orig/dist"
|
||||
```
|
||||
Check https://tauri.app/v1/api/config/#buildconfig for what these do, but our before build command `trunk build` will build into a folder `src-orig/dist` which we reference here.
|
||||
<br><br>
|
||||
Let's add a `Cargo.toml`` to both of our packages.
|
||||
|
||||
```sh
|
||||
touch src-tauri/Cargo.toml && touch src-orig/Cargo.toml
|
||||
```
|
||||
|
||||
Let's change `src-tauri/Cargo.toml` to this.
|
||||
|
||||
Let's change `src-tauri/Cargo.toml` to this, we're using the 2.0.0 alpha version of tauri to be able to build to mobile.
|
||||
```toml
|
||||
[package]
|
||||
name = "src_tauri"
|
||||
@@ -206,88 +164,86 @@ version = "0.0.1"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "app_lib"
|
||||
path = "src/lib.rs"
|
||||
name="app_lib"
|
||||
path="src/lib.rs"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.2.0", features = [] }
|
||||
tauri-build = { version = "2.0.0-alpha.13", features = [] }
|
||||
|
||||
[dependencies]
|
||||
log = "0.4.22"
|
||||
log = "0.4.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tauri = { version = "2.5.1", features = ["devtools"] }
|
||||
tauri-plugin-http = "2.4.4"
|
||||
tauri = { version = "2.0.0-alpha.20", features = ["devtools"] }
|
||||
tauri-plugin-http = "2.0.0-alpha.9"
|
||||
|
||||
[features]
|
||||
#default = ["custom-protocol"]
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
```
|
||||
|
||||
To make use of `cargo tauri build` we need `tauri-build` and we also need a `build.rs`.
|
||||
|
||||
```sh
|
||||
To make use of `cargo tauri build` we need `tauri-build` and we also need a `build.rs`
|
||||
```
|
||||
touch src-tauri/build.rs
|
||||
```
|
||||
|
||||
And let's change that to
|
||||
|
||||
```rust
|
||||
```
|
||||
fn main() {
|
||||
tauri_build::build();
|
||||
}
|
||||
```
|
||||
|
||||
In our `src-orig/Cargo.toml` let's add.
|
||||
|
||||
```toml
|
||||
```
|
||||
[package]
|
||||
name = "leptos_tauri_from_scratch"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
|
||||
[lib]
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[[bin]]
|
||||
name = "leptos_tauri_from_scratch_bin"
|
||||
path = "./src/main.rs"
|
||||
name="leptos_tauri_from_scratch_bin"
|
||||
path="./src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.8.4", optional = true }
|
||||
axum-macros = { version = "0.5.0", optional = true }
|
||||
console_error_panic_hook = { version = "0.1.7", optional = true }
|
||||
leptos = { git = "https://github.com/leptos-rs/leptos.git", rev = "v0.8.2" }
|
||||
leptos_axum = { git = "https://github.com/leptos-rs/leptos.git", rev = "v0.8.2", optional = true }
|
||||
leptos_meta = { git = "https://github.com/leptos-rs/leptos.git", rev = "v0.8.2", optional = true }
|
||||
server_fn = { git = "https://github.com/leptos-rs/leptos.git", rev = "v0.8.2", optional = true }
|
||||
tokio = { version = "1.45.1", features = ["rt-multi-thread"], optional = true }
|
||||
tower = { version = "0.5.2", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs", "cors"], optional = true }
|
||||
wasm-bindgen = { version = "=0.2.100", optional = true }
|
||||
axum = {version = "0.7.0", optional=true}
|
||||
axum-macros = { version= "0.4.1", optional=true}
|
||||
cfg-if = "1.0.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
console_log = "1.0.0"
|
||||
leptos = { git = "https://github.com/leptos-rs/leptos.git", branch = "leptos_v0.6" }
|
||||
leptos-use = "0.9.0"
|
||||
leptos_axum = { git = "https://github.com/leptos-rs/leptos.git", branch = "leptos_v0.6", optional = true }
|
||||
leptos_meta = { git = "https://github.com/leptos-rs/leptos.git", branch = "leptos_v0.6" }
|
||||
leptos_router = { git = "https://github.com/leptos-rs/leptos.git", branch = "leptos_v0.6" }
|
||||
log = "0.4.20"
|
||||
serde = "1.0.195"
|
||||
serde_json = "1.0.111"
|
||||
server_fn = { git = "https://github.com/leptos-rs/leptos.git", branch = "leptos_v0.6" }
|
||||
tokio = { version = "1.35.1", optional=true }
|
||||
tower = {version = "0.4.10", optional = true}
|
||||
tower-http = { version = "0.5.1", optional = true, features= ["fs","cors"] }
|
||||
wasm-bindgen = "0.2.89"
|
||||
|
||||
[features]
|
||||
csr = ["leptos/csr", "dep:server_fn"]
|
||||
hydrate = [
|
||||
"leptos/hydrate",
|
||||
"dep:leptos_meta",
|
||||
"dep:console_error_panic_hook",
|
||||
"dep:wasm-bindgen"
|
||||
]
|
||||
csr = [ "leptos/csr","leptos_meta/csr","leptos_router/csr", ]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:axum",
|
||||
"dep:axum-macros",
|
||||
"leptos/ssr",
|
||||
"dep:leptos_axum",
|
||||
"dep:leptos_meta",
|
||||
"leptos_meta/ssr",
|
||||
"dep:tower-http",
|
||||
"dep:tower",
|
||||
"dep:tokio",
|
||||
]
|
||||
"dep:axum",
|
||||
"dep:axum-macros",
|
||||
"leptos/ssr",
|
||||
"leptos-use/ssr",
|
||||
"dep:leptos_axum",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
"dep:tower-http",
|
||||
"dep:tower",
|
||||
"dep:tokio",
|
||||
]
|
||||
|
||||
[package.metadata.leptos]
|
||||
bin-exe-name = "leptos_tauri_from_scratch_bin"
|
||||
output-name = "leptos_tauri_from_scratch"
|
||||
bin-exe-name="leptos_tauri_from_scratch_bin"
|
||||
output-name="leptos_tauri_from_scratch"
|
||||
assets-dir = "../public"
|
||||
site-pkg-dir = "pkg"
|
||||
site-root = "target/site"
|
||||
@@ -301,240 +257,169 @@ bin-default-features = false
|
||||
lib-features = ["hydrate"]
|
||||
lib-default-features = false
|
||||
```
|
||||
|
||||
So this looks like a normal SSR leptos, except for our CSR, Hydrate, and SSR versions.
|
||||
|
||||
```toml
|
||||
csr = ["leptos/csr", "dep:server_fn"]
|
||||
hydrate = [
|
||||
"leptos/hydrate",
|
||||
"dep:leptos_meta",
|
||||
"dep:console_error_panic_hook",
|
||||
"dep:wasm-bindgen"
|
||||
]
|
||||
[features]
|
||||
csr = [ "leptos/csr","leptos_meta/csr","leptos_router/csr", ]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
```
|
||||
|
||||
also our binary is specified and named
|
||||
|
||||
```toml
|
||||
[[bin]]
|
||||
name="leptos_tauri_from_scratch_bin"
|
||||
path="./src/main.rs"
|
||||
```
|
||||
|
||||
our lib is specified, but unnamed (it will default to the project name in cargo leptos and in cargo tauri). We need the different crate types for `cargo leptos serve` and `cargo tauri build`
|
||||
|
||||
```toml
|
||||
[lib]
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
```
|
||||
|
||||
We've added the override to our cargo leptos metadata.
|
||||
|
||||
```toml
|
||||
[package.metadata.leptos]
|
||||
bin-exe-name="leptos_tauri_from_scratch_bin"
|
||||
```
|
||||
|
||||
Our tauri app is going to send server function calls to this address, this is where we'll serve our hydratable SSR client from.
|
||||
|
||||
```toml
|
||||
Our tauri app is going to send server function calls to this address, this is aksi where we'll serve our hydratable SSR client from.
|
||||
```
|
||||
site-addr = "0.0.0.0:3000"
|
||||
```
|
||||
|
||||
Now let's create the `main.rs` that we reference in the `src-orig/Cargo.toml`
|
||||
|
||||
```sh
|
||||
```
|
||||
mkdir src-orig/src && touch src-orig/src/main.rs
|
||||
```
|
||||
|
||||
and drop this in there...
|
||||
|
||||
```rust
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::{Request, State},
|
||||
response::IntoResponse,
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use leptos::logging::log;
|
||||
use leptos::prelude::*;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use leptos_tauri_from_scratch::{
|
||||
app::{shell, App},
|
||||
fallback::file_and_error_handler,
|
||||
};
|
||||
use tower_http::cors::CorsLayer;
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature="ssr")] {
|
||||
use tower_http::cors::{CorsLayer};
|
||||
use axum::{
|
||||
Router,
|
||||
routing::get,
|
||||
extract::State,
|
||||
http::Request,
|
||||
body::Body,
|
||||
response::IntoResponse
|
||||
};
|
||||
use leptos::{*,provide_context, LeptosOptions};
|
||||
use leptos_axum::LeptosRoutes;
|
||||
use leptos_tauri_from_scratch::fallback::file_and_error_handler;
|
||||
|
||||
let conf = get_configuration(None).unwrap();
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
let leptos_options = conf.leptos_options;
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(App);
|
||||
#[derive(Clone,Debug,axum_macros::FromRef)]
|
||||
pub struct ServerState{
|
||||
pub options:LeptosOptions,
|
||||
pub routes: Vec<leptos_router::RouteListing>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, axum_macros::FromRef)]
|
||||
pub struct ServerState {
|
||||
pub options: LeptosOptions,
|
||||
pub routes: Vec<leptos_axum::AxumRouteListing>,
|
||||
}
|
||||
|
||||
let state = ServerState {
|
||||
options: leptos_options,
|
||||
routes: routes.clone(),
|
||||
};
|
||||
|
||||
pub async fn server_fn_handler(
|
||||
State(state): State<ServerState>,
|
||||
request: Request<Body>,
|
||||
) -> impl IntoResponse {
|
||||
leptos_axum::handle_server_fns_with_context(
|
||||
move || {
|
||||
provide_context(state.clone());
|
||||
},
|
||||
request,
|
||||
)
|
||||
.await
|
||||
.into_response()
|
||||
}
|
||||
|
||||
let cors = CorsLayer::new()
|
||||
.allow_methods([axum::http::Method::GET, axum::http::Method::POST])
|
||||
.allow_origin(
|
||||
// Allow requests from the Tauri app
|
||||
"tauri://localhost"
|
||||
.parse::<axum::http::HeaderValue>()
|
||||
.unwrap(),
|
||||
)
|
||||
.allow_headers(vec![
|
||||
axum::http::header::CONTENT_TYPE,
|
||||
axum::http::header::ACCEPT,
|
||||
]);
|
||||
|
||||
pub async fn leptos_routes_handler(
|
||||
State(state): State<ServerState>,
|
||||
req: Request<Body>,
|
||||
) -> axum::response::Response {
|
||||
let leptos_options = state.options.clone();
|
||||
let handler = leptos_axum::render_route_with_context(
|
||||
state.routes.clone(),
|
||||
move || {
|
||||
provide_context("...");
|
||||
},
|
||||
move || shell(leptos_options.clone()),
|
||||
);
|
||||
handler(axum::extract::State(state), req)
|
||||
pub async fn server_fn_handler(
|
||||
State(state): State<ServerState>,
|
||||
request: Request<Body>,
|
||||
) -> impl IntoResponse {
|
||||
leptos_axum::handle_server_fns_with_context(
|
||||
move || {
|
||||
provide_context(state.clone());
|
||||
},
|
||||
request,
|
||||
)
|
||||
.await
|
||||
.into_response()
|
||||
}
|
||||
|
||||
pub async fn leptos_routes_handler(
|
||||
State(state): State<ServerState>,
|
||||
req: Request<Body>,
|
||||
) -> axum::response::Response {
|
||||
let handler = leptos_axum::render_route_with_context(
|
||||
state.options.clone(),
|
||||
state.routes.clone(),
|
||||
move || {
|
||||
provide_context("...");
|
||||
},
|
||||
leptos_tauri_from_scratch::App,
|
||||
);
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let conf = get_configuration(Some("./src-orig/Cargo.toml")).await.unwrap();
|
||||
|
||||
let leptos_options = conf.leptos_options;
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = leptos_axum::generate_route_list(leptos_tauri_from_scratch::App);
|
||||
|
||||
let state = ServerState{
|
||||
options:leptos_options,
|
||||
routes:routes.clone(),
|
||||
};
|
||||
|
||||
let cors = CorsLayer::new()
|
||||
.allow_methods([axum::http::Method::GET, axum::http::Method::POST])
|
||||
.allow_origin("tauri://localhost".parse::<axum::http::HeaderValue>().unwrap())
|
||||
.allow_headers(vec![axum::http::header::CONTENT_TYPE]);
|
||||
|
||||
|
||||
let app = Router::new()
|
||||
.route("/api/*fn_name",get(server_fn_handler).post(server_fn_handler))
|
||||
.layer(cors)
|
||||
.leptos_routes_with_handler(routes, get(leptos_routes_handler))
|
||||
.fallback(file_and_error_handler)
|
||||
.with_state(state);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
logging::log!("listening on http://{}", &addr);
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
} else if #[cfg(feature="csr")]{
|
||||
pub fn main() {
|
||||
server_fn::client::set_server_url("http://127.0.0.1:3000");
|
||||
leptos::mount_to_body(leptos_tauri_from_scratch::App);
|
||||
}
|
||||
} else {
|
||||
pub fn main() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
let app = Router::new()
|
||||
.route(
|
||||
"/api/{*fn_name}",
|
||||
get(server_fn_handler).post(server_fn_handler),
|
||||
)
|
||||
.layer(cors)
|
||||
.leptos_routes_with_handler(routes, get(leptos_routes_handler))
|
||||
.fallback(file_and_error_handler)
|
||||
.with_state(state);
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
log!("listening on http://{}", &addr);
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[cfg(feature = "csr")]
|
||||
pub fn main() {
|
||||
server_fn::client::set_server_url("http://127.0.0.1:3000");
|
||||
leptos::mount::mount_to_body(leptos_tauri_from_scratch::app::App);
|
||||
}
|
||||
```
|
||||
|
||||
and the hydration at `src-orig/src/lib.rs`
|
||||
|
||||
```rust
|
||||
pub mod app;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod fallback;
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
console_error_panic_hook::set_once();
|
||||
leptos::mount::hydrate_body(app::App);
|
||||
}
|
||||
```
|
||||
|
||||
This is our three pronged binary.
|
||||
|
||||
When we run cargo leptos server, we're going to get a server that is what's under `#[cfg(feature="ssr")]`.
|
||||
|
||||
And our csr feature
|
||||
|
||||
When we run cargo leptos server, we're going to get a server that is what's in our `if #[cfg(feature="ssr")] {` branch. We're going to hydrate, that's final `else` branch that is just empty. That actually gets filled in or something with a call to hydrate.
|
||||
<br>
|
||||
And our csr feature
|
||||
```rust
|
||||
#[cfg(feature = "csr")]
|
||||
pub fn main() {
|
||||
server_fn::client::set_server_url("http://127.0.0.1:3000");
|
||||
leptos::mount::mount_to_body(leptos_tauri_from_scratch::app::App);
|
||||
}
|
||||
else if #[cfg(feature="csr")]{
|
||||
pub fn main() {
|
||||
server_fn::client::set_server_url("http://127.0.0.1:3000");
|
||||
leptos::mount_to_body(leptos_tauri_from_scratch::App);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Here we're setting the server functions to use the url base that we access in our browser. I.e local host, on the port we specified in the leptos metadata.
|
||||
Here we're setting the server functions to use the url base that we access in our browser. I.e local host, on the port we specified in the leptos metadata.<br>
|
||||
Otherwise our tauri app will try to route server function network requests using it's own idea of what it's url is. Which is `tauri://localhost` on macOS, and something else on windows.
|
||||
|
||||
<br>
|
||||
Since we are going to be getting API requests from different locations beside our server's domain let's set up CORS, if you don't do this your tauri apps won't be able to make server function calls because it will run into CORS erros.
|
||||
|
||||
```rust
|
||||
let cors = CorsLayer::new()
|
||||
.allow_methods([axum::http::Method::GET, axum::http::Method::POST])
|
||||
.allow_origin(
|
||||
"tauri://localhost"
|
||||
.parse::<axum::http::HeaderValue>()
|
||||
.unwrap(),
|
||||
)
|
||||
.allow_headers(vec![axum::http::header::CONTENT_TYPE]);
|
||||
let cors = CorsLayer::new()
|
||||
.allow_methods([axum::http::Method::GET, axum::http::Method::POST])
|
||||
.allow_origin("tauri://localhost".parse::<axum::http::HeaderValue>().unwrap())
|
||||
.allow_headers(vec![axum::http::header::CONTENT_TYPE]);
|
||||
```
|
||||
|
||||
If you are on windows the origin of your app will be different than `tauri://localhost` and you'll need to figure that out, as well as if you deploy it to places that aren't your localhost!
|
||||
|
||||
If you are on windows the origin of your app will be different then `tauri://localhost` and you'll need to figure that out, as well as if you deploy it to places that aren't your localhost!
|
||||
<br>
|
||||
Everything else is standard leptos, so let's fill in the fallback and the lib really quick.
|
||||
|
||||
```sh
|
||||
touch src-orig/src/lib.rs && touch src-orig/src/fallback.rs
|
||||
```
|
||||
|
||||
Let's dump this bog standard leptos code in the `src-orig/src/app.rs`
|
||||
|
||||
Let's dump this bog standard leptos code in the `src-orig/src/lib.rs``
|
||||
```rust
|
||||
use leptos::prelude::*;
|
||||
use leptos::*;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
use leptos_meta::MetaTags;
|
||||
view! {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<AutoReload options=options.clone() />
|
||||
<HydrationScripts options/>
|
||||
<MetaTags/>
|
||||
</head>
|
||||
<body>
|
||||
<App/>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
}
|
||||
pub mod fallback;
|
||||
|
||||
#[server(endpoint = "hello_world")]
|
||||
pub async fn hello_world_server() -> Result<String, ServerFnError> {
|
||||
@@ -543,9 +428,9 @@ pub async fn hello_world_server() -> Result<String, ServerFnError> {
|
||||
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
let action = ServerAction::<HelloWorldServer>::new();
|
||||
let vals = RwSignal::new(String::new());
|
||||
Effect::new(move |_| {
|
||||
let action = create_server_action::<HelloWorldServer>();
|
||||
let vals = create_rw_signal(String::new());
|
||||
create_effect(move |_| {
|
||||
if let Some(resp) = action.value().get() {
|
||||
match resp {
|
||||
Ok(val) => vals.set(val),
|
||||
@@ -553,22 +438,31 @@ pub fn App() -> impl IntoView {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
view! {
|
||||
<button
|
||||
on:click=move |_| {
|
||||
action.dispatch(HelloWorldServer{});
|
||||
}
|
||||
view! {<button
|
||||
on:click=move |_| {
|
||||
action.dispatch(HelloWorldServer{});
|
||||
}
|
||||
>"Hello world."</button>
|
||||
<br/><br/>
|
||||
<span>"Server says: "</span>
|
||||
{move || vals.get()}
|
||||
{
|
||||
move || vals.get()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "hydrate")] {
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
#[cfg(debug_assertions)]
|
||||
console_error_panic_hook::set_once();
|
||||
leptos::mount_to_body(App);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
and add this to `src-org/src/fallback.rs`
|
||||
|
||||
```rust
|
||||
use axum::{
|
||||
body::Body,
|
||||
@@ -576,7 +470,7 @@ use axum::{
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
response::{IntoResponse, Response as AxumResponse},
|
||||
};
|
||||
use leptos::{view, prelude::LeptosOptions};
|
||||
use leptos::{view, LeptosOptions};
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
@@ -591,17 +485,12 @@ pub async fn file_and_error_handler(
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else {
|
||||
let handler = leptos_axum::render_app_to_stream(
|
||||
move || view! {404},
|
||||
);
|
||||
let handler = leptos_axum::render_app_to_stream(options.to_owned(), move || view! {404});
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_static_file(
|
||||
uri: Uri,
|
||||
root: &str,
|
||||
) -> Result<Response<Body>, (StatusCode, String)> {
|
||||
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<Body>, (StatusCode, String)> {
|
||||
let req = Request::builder()
|
||||
.uri(uri.clone())
|
||||
.body(Body::empty())
|
||||
@@ -615,15 +504,11 @@ async fn get_static_file(
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Let's fill in our `src-tauri/src/` folder.
|
||||
|
||||
```sh
|
||||
Let's fill in our src-tauri/src folder.
|
||||
```
|
||||
mkdir src-tauri/src && touch src-tauri/src/main.rs && touch src-tauri/src/lib.rs
|
||||
```
|
||||
|
||||
and drop this in `src-tauri/src/main.rs` This is standard tauri boilerplate.
|
||||
|
||||
```rust
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
@@ -632,9 +517,7 @@ fn main() {
|
||||
app_lib::run();
|
||||
}
|
||||
```
|
||||
|
||||
and in `src-tauri/src/lib.rs`
|
||||
|
||||
```rust
|
||||
use tauri::Manager;
|
||||
|
||||
@@ -644,7 +527,7 @@ pub fn run() {
|
||||
.plugin(tauri_plugin_http::init())
|
||||
.setup(|app| {
|
||||
{
|
||||
let window = app.get_webview_window("main").unwrap();
|
||||
let window = app.get_window("main").unwrap();
|
||||
window.open_devtools();
|
||||
}
|
||||
Ok(())
|
||||
@@ -653,30 +536,21 @@ pub fn run() {
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
```
|
||||
|
||||
We're gonna open devtools right away to see what is going on in our app. We need the tauri_http_plugin to make http calls, and generate_context reads our `tauri.conf.json` in the package in which its run.
|
||||
|
||||
<br><br>
|
||||
We need an icon folder and an icon to build.
|
||||
|
||||
```sh
|
||||
mkdir src-tauri/icons
|
||||
curl https://raw.githubusercontent.com/tauri-apps/tauri/dev/examples/.icons/128x128.png > src-tauri/icons/icon.png
|
||||
mkdir src-tauri/icons && curl https://raw.githubusercontent.com/tauri-apps/tauri/dev/examples/.icons/128x128.png > src-tauri/icons/icon.png
|
||||
```
|
||||
|
||||
set nightly
|
||||
|
||||
```sh
|
||||
rustup override set nightly
|
||||
```
|
||||
|
||||
Then run
|
||||
|
||||
Then run
|
||||
```sh
|
||||
cargo leptos serve
|
||||
```
|
||||
|
||||
You should get something like
|
||||
|
||||
You should get
|
||||
```sh
|
||||
➜ lepto_tauri_from_scratch git:(main) ✗ cargo leptos serve
|
||||
Finished dev [unoptimized + debuginfo] target(s) in 0.60s
|
||||
@@ -687,33 +561,24 @@ You should get something like
|
||||
Serving at http://0.0.0.0:3000
|
||||
listening on http://0.0.0.0:3000
|
||||
```
|
||||
|
||||
Now open a new terminal and
|
||||
|
||||
```sh
|
||||
cargo tauri build
|
||||
```
|
||||
|
||||
> Install `tauri-cli` if you haven't already.
|
||||
|
||||
It'll build with csr before
|
||||
|
||||
```sh
|
||||
Running beforeBuildCommand `trunk build --no-default-features -v --features "csr"`
|
||||
```
|
||||
|
||||
and then you should have your app, I'm on macOS so here's what I get. It's for desktop.
|
||||
|
||||
```sh
|
||||
```
|
||||
Compiling src_tauri v0.0.1 (/Users/sam/Projects/lepto_tauri_from_scratch/src-tauri)
|
||||
Finished release [optimized] target(s) in 2m 26s
|
||||
Bundling leptos_tauri_from_scratch.app (/Users/sam/Projects/lepto_tauri_from_scratch/target/release/bundle/macos/leptos_tauri_from_scratch.app)
|
||||
Bundling leptos_tauri_from_scratch_0.1.0_x64.dmg (/Users/sam/Projects/lepto_tauri_from_scratch/target/release/bundle/dmg/leptos_tauri_from_scratch_0.1.0_x64.dmg)
|
||||
Running bundle_dmg.sh
|
||||
```
|
||||
|
||||
Open run it and voilá. Click hello world button and read "Hey" from the server.
|
||||
|
||||
## Thoughts, Feedback, Criticism, Comments?
|
||||
|
||||
Send me any of the above, I'm @sjud on leptos discord. I'm always looking to improve and make these projects more helpful for the community. So please let me know how I can do that. Thanks!
|
||||
## Thoughts, Feedback, Criticism, Comments?
|
||||
Send me any of the above, I'm @sjud on leptos discord. I'm always looking to improve and make these projects more helpful for the community. So please let me know how I can do that. Thanks!
|
||||
@@ -3,6 +3,7 @@ name = "leptos_tauri_from_scratch"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
|
||||
[lib]
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
@@ -11,35 +12,43 @@ name = "leptos_tauri_from_scratch_bin"
|
||||
path = "./src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.8.4", optional = true }
|
||||
axum-macros = { version = "0.5.0", optional = true }
|
||||
console_error_panic_hook = { version = "0.1.7", optional = true }
|
||||
leptos = { git = "https://github.com/leptos-rs/leptos.git", rev = "v0.8.2" }
|
||||
leptos_axum = { git = "https://github.com/leptos-rs/leptos.git", rev = "v0.8.2", optional = true }
|
||||
leptos_meta = { git = "https://github.com/leptos-rs/leptos.git", rev = "v0.8.2", optional = true }
|
||||
server_fn = { git = "https://github.com/leptos-rs/leptos.git", rev = "v0.8.2", optional = true }
|
||||
tokio = { version = "1.45.1", features = ["rt-multi-thread"], optional = true }
|
||||
tower = { version = "0.5.2", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs", "cors"], optional = true }
|
||||
wasm-bindgen = { version = "=0.2.100", optional = true }
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
axum-macros = { version = "0.4.1", optional = true }
|
||||
cfg-if = "1.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
console_log = "1.0"
|
||||
leptos = { git = "https://github.com/leptos-rs/leptos.git", branch = "leptos_v0.6" }
|
||||
leptos-use = "0.11.3"
|
||||
leptos_axum = { git = "https://github.com/leptos-rs/leptos.git", branch = "leptos_v0.6", optional = true }
|
||||
leptos_meta = { git = "https://github.com/leptos-rs/leptos.git", branch = "leptos_v0.6" }
|
||||
leptos_router = { git = "https://github.com/leptos-rs/leptos.git", branch = "leptos_v0.6" }
|
||||
log = "0.4.22"
|
||||
sqlx = { version = "0.8.0", optional = true, features = [
|
||||
"sqlite",
|
||||
"runtime-tokio",
|
||||
] }
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
server_fn = { git = "https://github.com/leptos-rs/leptos.git", branch = "leptos_v0.6" }
|
||||
tokio = { version = "1.39", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", optional = true, features = ["fs", "cors"] }
|
||||
wasm-bindgen = "0.2.92"
|
||||
|
||||
[features]
|
||||
csr = ["leptos/csr", "dep:server_fn"]
|
||||
hydrate = [
|
||||
"leptos/hydrate",
|
||||
"dep:leptos_meta",
|
||||
"dep:console_error_panic_hook",
|
||||
"dep:wasm-bindgen"
|
||||
]
|
||||
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:axum",
|
||||
"dep:axum-macros",
|
||||
"leptos/ssr",
|
||||
"leptos-use/ssr",
|
||||
"dep:leptos_axum",
|
||||
"dep:leptos_meta",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
"dep:tower-http",
|
||||
"dep:tower",
|
||||
"dep:sqlx",
|
||||
"dep:tokio",
|
||||
]
|
||||
|
||||
@@ -58,3 +67,4 @@ bin-features = ["ssr"]
|
||||
bin-default-features = false
|
||||
lib-features = ["hydrate"]
|
||||
lib-default-features = false
|
||||
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
use leptos::prelude::*;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
use leptos_meta::MetaTags;
|
||||
view! {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<AutoReload options=options.clone() />
|
||||
<HydrationScripts options/>
|
||||
<MetaTags/>
|
||||
</head>
|
||||
<body>
|
||||
<App/>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
}
|
||||
|
||||
#[server(endpoint = "hello_world")]
|
||||
pub async fn hello_world_server() -> Result<String, ServerFnError> {
|
||||
Ok("Hey.".to_string())
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
let action = ServerAction::<HelloWorldServer>::new();
|
||||
let vals = RwSignal::new(String::new());
|
||||
Effect::new(move |_| {
|
||||
if let Some(resp) = action.value().get() {
|
||||
match resp {
|
||||
Ok(val) => vals.set(val),
|
||||
Err(err) => vals.set(format!("{err:?}")),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
view! {
|
||||
<button
|
||||
on:click=move |_| {
|
||||
action.dispatch(HelloWorldServer{});
|
||||
}
|
||||
>"Hello world."</button>
|
||||
<br/><br/>
|
||||
<span>"Server says: "</span>
|
||||
{move || vals.get()}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ use axum::{
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
response::{IntoResponse, Response as AxumResponse},
|
||||
};
|
||||
use leptos::{prelude::LeptosOptions, view};
|
||||
use leptos::{view, LeptosOptions};
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
@@ -19,15 +19,12 @@ pub async fn file_and_error_handler(
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else {
|
||||
let handler = leptos_axum::render_app_to_stream(move || view! {404});
|
||||
let handler = leptos_axum::render_app_to_stream(options.to_owned(), move || view! {404});
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_static_file(
|
||||
uri: Uri,
|
||||
root: &str,
|
||||
) -> Result<Response<Body>, (StatusCode, String)> {
|
||||
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<Body>, (StatusCode, String)> {
|
||||
let req = Request::builder()
|
||||
.uri(uri.clone())
|
||||
.body(Body::empty())
|
||||
@@ -39,4 +36,4 @@ async fn get_static_file(
|
||||
format!("Something went wrong: {err}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,44 @@
|
||||
pub mod app;
|
||||
use leptos::*;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod fallback;
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
console_error_panic_hook::set_once();
|
||||
leptos::mount::hydrate_body(app::App);
|
||||
#[server(endpoint = "hello_world")]
|
||||
pub async fn hello_world_server() -> Result<String, ServerFnError> {
|
||||
Ok("Hey.".to_string())
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
let action = create_server_action::<HelloWorldServer>();
|
||||
let vals = create_rw_signal(String::new());
|
||||
create_effect(move |_| {
|
||||
if let Some(resp) = action.value().get() {
|
||||
match resp {
|
||||
Ok(val) => vals.set(val),
|
||||
Err(err) => vals.set(format!("{err:?}")),
|
||||
}
|
||||
}
|
||||
});
|
||||
view! {<button
|
||||
on:click=move |_| {
|
||||
action.dispatch(HelloWorldServer{});
|
||||
}
|
||||
>"Hello world."</button>
|
||||
{
|
||||
move || vals.get()
|
||||
}
|
||||
}
|
||||
}
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "hydrate")] {
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
#[cfg(debug_assertions)]
|
||||
console_error_panic_hook::set_once();
|
||||
leptos::mount_to_body(App);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,100 +1,93 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::{Request, State},
|
||||
response::IntoResponse,
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use leptos::logging::log;
|
||||
use leptos::prelude::*;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use leptos_tauri_from_scratch::{
|
||||
app::{shell, App},
|
||||
fallback::file_and_error_handler,
|
||||
};
|
||||
use tower_http::cors::CorsLayer;
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature="ssr")] {
|
||||
use tower_http::cors::{CorsLayer};
|
||||
use axum::{
|
||||
Router,
|
||||
routing::get,
|
||||
extract::State,
|
||||
http::Request,
|
||||
body::Body,
|
||||
response::IntoResponse
|
||||
};
|
||||
use leptos::{*,provide_context, LeptosOptions};
|
||||
use leptos_axum::LeptosRoutes;
|
||||
use leptos_tauri_from_scratch::fallback::file_and_error_handler;
|
||||
|
||||
let conf = get_configuration(None).unwrap();
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
let leptos_options = conf.leptos_options;
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(App);
|
||||
#[derive(Clone,Debug,axum_macros::FromRef)]
|
||||
pub struct ServerState{
|
||||
pub options:LeptosOptions,
|
||||
pub routes: Vec<leptos_router::RouteListing>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, axum_macros::FromRef)]
|
||||
pub struct ServerState {
|
||||
pub options: LeptosOptions,
|
||||
pub routes: Vec<leptos_axum::AxumRouteListing>,
|
||||
}
|
||||
|
||||
let state = ServerState {
|
||||
options: leptos_options,
|
||||
routes: routes.clone(),
|
||||
};
|
||||
|
||||
pub async fn server_fn_handler(
|
||||
State(state): State<ServerState>,
|
||||
request: Request<Body>,
|
||||
) -> impl IntoResponse {
|
||||
leptos_axum::handle_server_fns_with_context(
|
||||
move || {
|
||||
provide_context(state.clone());
|
||||
},
|
||||
request,
|
||||
)
|
||||
.await
|
||||
.into_response()
|
||||
}
|
||||
|
||||
let cors = CorsLayer::new()
|
||||
.allow_methods([axum::http::Method::GET, axum::http::Method::POST])
|
||||
.allow_origin(
|
||||
"tauri://localhost"
|
||||
.parse::<axum::http::HeaderValue>()
|
||||
.unwrap(),
|
||||
)
|
||||
.allow_headers(vec![axum::http::header::CONTENT_TYPE]);
|
||||
|
||||
pub async fn leptos_routes_handler(
|
||||
State(state): State<ServerState>,
|
||||
req: Request<Body>,
|
||||
) -> axum::response::Response {
|
||||
let leptos_options = state.options.clone();
|
||||
let handler = leptos_axum::render_route_with_context(
|
||||
state.routes.clone(),
|
||||
move || {
|
||||
provide_context("...");
|
||||
},
|
||||
move || shell(leptos_options.clone()),
|
||||
);
|
||||
handler(axum::extract::State(state), req)
|
||||
pub async fn server_fn_handler(
|
||||
State(state): State<ServerState>,
|
||||
request: Request<Body>,
|
||||
) -> impl IntoResponse {
|
||||
leptos_axum::handle_server_fns_with_context(
|
||||
move || {
|
||||
provide_context(state.clone());
|
||||
},
|
||||
request,
|
||||
)
|
||||
.await
|
||||
.into_response()
|
||||
}
|
||||
|
||||
pub async fn leptos_routes_handler(
|
||||
State(state): State<ServerState>,
|
||||
req: Request<Body>,
|
||||
) -> axum::response::Response {
|
||||
let handler = leptos_axum::render_route_with_context(
|
||||
state.options.clone(),
|
||||
state.routes.clone(),
|
||||
move || {
|
||||
provide_context("...");
|
||||
},
|
||||
leptos_tauri_from_scratch::App,
|
||||
);
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let conf = get_configuration(Some("./src-orig/Cargo.toml")).await.unwrap();
|
||||
|
||||
let leptos_options = conf.leptos_options;
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = leptos_axum::generate_route_list(leptos_tauri_from_scratch::App);
|
||||
|
||||
let state = ServerState{
|
||||
options:leptos_options,
|
||||
routes:routes.clone(),
|
||||
};
|
||||
|
||||
let cors = CorsLayer::new()
|
||||
.allow_methods([axum::http::Method::GET, axum::http::Method::POST])
|
||||
.allow_origin("tauri://localhost".parse::<axum::http::HeaderValue>().unwrap())
|
||||
.allow_headers(vec![axum::http::header::CONTENT_TYPE]);
|
||||
|
||||
|
||||
let app = Router::new()
|
||||
.route("/api/*fn_name",get(server_fn_handler).post(server_fn_handler))
|
||||
.layer(cors)
|
||||
.leptos_routes_with_handler(routes, get(leptos_routes_handler))
|
||||
.fallback(file_and_error_handler)
|
||||
.with_state(state);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
logging::log!("listening on http://{}", &addr);
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
} else if #[cfg(feature="csr")]{
|
||||
pub fn main() {
|
||||
server_fn::client::set_server_url("http://127.0.0.1:3000");
|
||||
leptos::mount_to_body(leptos_tauri_from_scratch::App);
|
||||
}
|
||||
} else {
|
||||
pub fn main() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
let app = Router::new()
|
||||
.route(
|
||||
"/api/{*fn_name}",
|
||||
get(server_fn_handler).post(server_fn_handler),
|
||||
)
|
||||
.layer(cors)
|
||||
.leptos_routes_with_handler(routes, get(leptos_routes_handler))
|
||||
.fallback(file_and_error_handler)
|
||||
.with_state(state);
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
log!("listening on http://{}", &addr);
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[cfg(feature = "csr")]
|
||||
pub fn main() {
|
||||
server_fn::client::set_server_url("http://127.0.0.1:3000");
|
||||
leptos::mount::mount_to_body(leptos_tauri_from_scratch::app::App);
|
||||
}
|
||||
}
|
||||
@@ -8,13 +8,13 @@ name = "app_lib"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.2.0", features = [] }
|
||||
tauri-build = { version = "2.0.0-rc.2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
log = "0.4.22"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tauri = { version = "2.5.1", features = ["devtools"] }
|
||||
tauri-plugin-http = "2.4.4"
|
||||
tauri = { version = "2.0.0-rc.2", features = ["devtools"] }
|
||||
tauri-plugin-http = "2.0.0-rc.0"
|
||||
|
||||
[features]
|
||||
#default = ["custom-protocol"]
|
||||
|
||||
@@ -6,11 +6,11 @@ pub fn run() {
|
||||
.plugin(tauri_plugin_http::init())
|
||||
.setup(|app| {
|
||||
{
|
||||
let window = app.get_webview_window("main").unwrap();
|
||||
let window = app.get_window("main").unwrap();
|
||||
window.open_devtools();
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
}
|
||||
@@ -3,4 +3,4 @@
|
||||
|
||||
fn main() {
|
||||
app_lib::run();
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,42 @@
|
||||
{
|
||||
"identifier": "leptos.chat.app",
|
||||
"productName": "leptos_tauri_from_scratch",
|
||||
"version": "0.1.0",
|
||||
"build": {
|
||||
"beforeDevCommand": "",
|
||||
"beforeBuildCommand": "trunk build --no-default-features -v --features \"csr\"",
|
||||
"devUrl": "http://127.0.0.1:3000",
|
||||
"frontendDist": "../dist"
|
||||
"devPath": "http://127.0.0.1:3000",
|
||||
"distDir": "../src-orig/dist"
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"category": "DeveloperTool",
|
||||
"copyright": "",
|
||||
"externalBin": [],
|
||||
"icon": ["icons/icon.png"],
|
||||
"longDescription": "",
|
||||
"macOS": {
|
||||
"entitlements": null,
|
||||
"exceptionDomain": "",
|
||||
"frameworks": [],
|
||||
"providerShortName": null,
|
||||
"signingIdentity": null
|
||||
"package": {
|
||||
"productName": "leptos_tauri_from_scratch",
|
||||
"version": "0.1.0"
|
||||
},
|
||||
"tauri": {
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"category": "DeveloperTool",
|
||||
"copyright": "",
|
||||
"deb": {
|
||||
"depends": []
|
||||
},
|
||||
"externalBin": [],
|
||||
"icon": [],
|
||||
"identifier": "leptos.chat.app",
|
||||
"longDescription": "",
|
||||
"macOS": {
|
||||
"entitlements": null,
|
||||
"exceptionDomain": "",
|
||||
"frameworks": [],
|
||||
"providerShortName": null,
|
||||
"signingIdentity": null
|
||||
},
|
||||
"resources": [],
|
||||
"shortDescription": "",
|
||||
"targets": "all",
|
||||
"windows": {
|
||||
"certificateThumbprint": null,
|
||||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": ""
|
||||
}
|
||||
},
|
||||
"resources": [],
|
||||
"shortDescription": "",
|
||||
"targets": "all",
|
||||
"windows": {
|
||||
"certificateThumbprint": null,
|
||||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": ""
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"security": {
|
||||
"csp": null
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "reactive_graph"
|
||||
version = "0.2.2"
|
||||
version = "0.2.0-rc3"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -12,28 +12,28 @@ edition.workspace = true
|
||||
[dependencies]
|
||||
any_spawner = { workspace = true }
|
||||
or_poisoned = { workspace = true }
|
||||
futures = { workspace = true, default-features = true }
|
||||
futures = "0.3.31"
|
||||
hydration_context = { workspace = true, optional = true }
|
||||
pin-project-lite = { workspace = true, default-features = true }
|
||||
rustc-hash = { workspace = true, default-features = true }
|
||||
serde = { features = ["derive"], optional = true , workspace = true, default-features = true }
|
||||
slotmap = { workspace = true, default-features = true }
|
||||
thiserror = { workspace = true , default-features = true }
|
||||
tracing = { optional = true , workspace = true, default-features = true }
|
||||
guardian = { workspace = true, default-features = true }
|
||||
async-lock = { workspace = true, default-features = true }
|
||||
send_wrapper = { features = ["futures"] , workspace = true, default-features = true }
|
||||
pin-project-lite = "0.2.15"
|
||||
rustc-hash = "2.0"
|
||||
serde = { version = "1.0", features = ["derive"], optional = true }
|
||||
slotmap = "1.0"
|
||||
thiserror = { workspace = true }
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
guardian = "1.2"
|
||||
async-lock = "3.4.0"
|
||||
send_wrapper = { version = "0.6.0", features = ["futures"] }
|
||||
|
||||
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
|
||||
web-sys = { version = "0.3.77", features = ["console"] }
|
||||
web-sys = { version = "0.3.72", features = ["console"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { features = ["rt-multi-thread", "macros"] , workspace = true, default-features = true }
|
||||
tokio-test = { workspace = true, default-features = true }
|
||||
tokio = { version = "1.43", features = ["rt-multi-thread", "macros"] }
|
||||
tokio-test = { version = "0.4.4" }
|
||||
any_spawner = { workspace = true, features = ["futures-executor", "tokio"] }
|
||||
|
||||
[build-dependencies]
|
||||
rustc_version = { workspace = true, default-features = true }
|
||||
rustc_version = "0.4.1"
|
||||
|
||||
[features]
|
||||
nightly = []
|
||||
|
||||
@@ -234,8 +234,7 @@ macro_rules! spawn_derived {
|
||||
subscribers: SubscriberSet::new(),
|
||||
state: AsyncDerivedState::Clean,
|
||||
version: 0,
|
||||
suspenses: Vec::new(),
|
||||
pending_suspenses: Vec::new()
|
||||
suspenses: Vec::new()
|
||||
}));
|
||||
let value = Arc::new(AsyncRwLock::new($initial));
|
||||
let wakers = Arc::new(RwLock::new(Vec::new()));
|
||||
@@ -365,7 +364,7 @@ macro_rules! spawn_derived {
|
||||
// generate and assign new value
|
||||
loading.store(true, Ordering::Relaxed);
|
||||
|
||||
let this_version = {
|
||||
let (this_version, suspense_ids) = {
|
||||
let mut guard = inner.write().or_poisoned();
|
||||
guard.version += 1;
|
||||
let version = guard.version;
|
||||
@@ -373,17 +372,14 @@ macro_rules! spawn_derived {
|
||||
.into_iter()
|
||||
.map(|sc| sc.task_id())
|
||||
.collect::<Vec<_>>();
|
||||
guard.pending_suspenses.extend(suspense_ids);
|
||||
version
|
||||
(version, suspense_ids)
|
||||
};
|
||||
|
||||
let new_value = fut.await;
|
||||
|
||||
let latest_version = {
|
||||
let mut guard = inner.write().or_poisoned();
|
||||
drop(mem::take(&mut guard.pending_suspenses));
|
||||
guard.version
|
||||
};
|
||||
drop(suspense_ids);
|
||||
|
||||
let latest_version = inner.read().or_poisoned().version;
|
||||
|
||||
if latest_version == this_version {
|
||||
Self::set_inner_value(new_value, value, wakers, inner, loading, Some(ready_tx)).await;
|
||||
@@ -484,10 +480,7 @@ impl<T: 'static> ArcAsyncDerived<T> {
|
||||
{
|
||||
let fun = move || {
|
||||
let fut = fun();
|
||||
let fut = async move { SendOption::new(Some(fut.await)) };
|
||||
#[cfg(feature = "sandboxed-arenas")]
|
||||
let fut = Sandboxed::new(fut);
|
||||
fut
|
||||
async move { SendOption::new(Some(fut.await)) }
|
||||
};
|
||||
let initial_value = SendOption::new(initial_value);
|
||||
let (this, _) = spawn_derived!(
|
||||
@@ -521,12 +514,7 @@ impl<T: 'static> ArcAsyncDerived<T> {
|
||||
{
|
||||
let fun = move || {
|
||||
let fut = fun();
|
||||
let fut = ScopedFuture::new_untracked(async move {
|
||||
SendOption::new(Some(fut.await))
|
||||
});
|
||||
#[cfg(feature = "sandboxed-arenas")]
|
||||
let fut = Sandboxed::new(fut);
|
||||
fut
|
||||
async move { SendOption::new(Some(fut.await)) }
|
||||
};
|
||||
let initial_value = SendOption::new(initial_value);
|
||||
let (this, _) = spawn_derived!(
|
||||
@@ -568,10 +556,7 @@ impl<T: 'static> ArcAsyncDerived<T> {
|
||||
{
|
||||
let fun = move || {
|
||||
let fut = fun();
|
||||
let fut = async move { SendOption::new_local(Some(fut.await)) };
|
||||
#[cfg(feature = "sandboxed-arenas")]
|
||||
let fut = Sandboxed::new(fut);
|
||||
fut
|
||||
async move { SendOption::new_local(Some(fut.await)) }
|
||||
};
|
||||
let initial_value = SendOption::new_local(initial_value);
|
||||
let (this, _) = spawn_derived!(
|
||||
@@ -607,10 +592,7 @@ impl<T: 'static> ArcAsyncDerived<T> {
|
||||
let initial = SendOption::new_local(None::<T>);
|
||||
let fun = move || {
|
||||
let fut = fun();
|
||||
let fut = async move { SendOption::new_local(Some(fut.await)) };
|
||||
#[cfg(feature = "sandboxed-arenas")]
|
||||
let fut = Sandboxed::new(fut);
|
||||
fut
|
||||
async move { SendOption::new_local(Some(fut.await)) }
|
||||
};
|
||||
let (this, _) = spawn_derived!(
|
||||
crate::spawn_local,
|
||||
@@ -659,14 +641,6 @@ impl<T: 'static> Write for ArcAsyncDerived<T> {
|
||||
type Value = Option<T>;
|
||||
|
||||
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
|
||||
// increment the version, such that a rerun triggered previously does not overwrite this
|
||||
// new value
|
||||
let mut guard = self.inner.write().or_poisoned();
|
||||
guard.version += 1;
|
||||
|
||||
// tell any suspenses to stop waiting for this
|
||||
drop(mem::take(&mut guard.pending_suspenses));
|
||||
|
||||
Some(MappedMut::new(
|
||||
WriteGuard::new(self.clone(), self.value.blocking_write()),
|
||||
|v| v.deref(),
|
||||
@@ -677,14 +651,6 @@ impl<T: 'static> Write for ArcAsyncDerived<T> {
|
||||
fn try_write_untracked(
|
||||
&self,
|
||||
) -> Option<impl DerefMut<Target = Self::Value>> {
|
||||
// increment the version, such that a rerun triggered previously does not overwrite this
|
||||
// new value
|
||||
let mut guard = self.inner.write().or_poisoned();
|
||||
guard.version += 1;
|
||||
|
||||
// tell any suspenses to stop waiting for this
|
||||
drop(mem::take(&mut guard.pending_suspenses));
|
||||
|
||||
Some(MappedMut::new(
|
||||
self.value.blocking_write(),
|
||||
|v| v.deref(),
|
||||
|
||||
@@ -14,10 +14,8 @@ use crate::{
|
||||
unwrap_signal,
|
||||
};
|
||||
use core::fmt::Debug;
|
||||
use or_poisoned::OrPoisoned;
|
||||
use std::{
|
||||
future::Future,
|
||||
mem,
|
||||
ops::{Deref, DerefMut},
|
||||
panic::Location,
|
||||
};
|
||||
@@ -29,7 +27,7 @@ use std::{
|
||||
/// values that depend on it that it has changed.
|
||||
///
|
||||
/// This is an arena-allocated type, which is `Copy` and is disposed when its reactive
|
||||
/// [`Owner`](crate::owner::Owner) cleans up. For a reference-counted signal that lives as
|
||||
/// [`Owner`](crate::owner::Owner) cleans up. For a reference-counted signal that livesas
|
||||
/// as long as a reference to it is alive, see [`ArcAsyncDerived`].
|
||||
///
|
||||
/// ## Examples
|
||||
@@ -351,17 +349,6 @@ where
|
||||
let guard = self
|
||||
.inner
|
||||
.try_with_value(|n| n.value.blocking_write_arc())?;
|
||||
|
||||
self.inner.try_with_value(|n| {
|
||||
let mut guard = n.inner.write().or_poisoned();
|
||||
// increment the version, such that a rerun triggered previously does not overwrite this
|
||||
// new value
|
||||
guard.version += 1;
|
||||
|
||||
// tell any suspenses to stop waiting for this
|
||||
drop(mem::take(&mut guard.pending_suspenses));
|
||||
});
|
||||
|
||||
Some(MappedMut::new(
|
||||
WriteGuard::new(*self, guard),
|
||||
|v| v.deref(),
|
||||
@@ -372,16 +359,6 @@ where
|
||||
fn try_write_untracked(
|
||||
&self,
|
||||
) -> Option<impl DerefMut<Target = Self::Value>> {
|
||||
self.inner.try_with_value(|n| {
|
||||
let mut guard = n.inner.write().or_poisoned();
|
||||
// increment the version, such that a rerun triggered previously does not overwrite this
|
||||
// new value
|
||||
guard.version += 1;
|
||||
|
||||
// tell any suspenses to stop waiting for this
|
||||
drop(mem::take(&mut guard.pending_suspenses));
|
||||
});
|
||||
|
||||
self.inner
|
||||
.try_with_value(|n| n.value.blocking_write_arc())
|
||||
.map(|inner| {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use super::suspense::TaskHandle;
|
||||
use crate::{
|
||||
channel::Sender,
|
||||
computed::suspense::SuspenseContext,
|
||||
@@ -23,7 +22,6 @@ pub(crate) struct ArcAsyncDerivedInner {
|
||||
pub state: AsyncDerivedState,
|
||||
pub version: usize,
|
||||
pub suspenses: Vec<SuspenseContext>,
|
||||
pub pending_suspenses: Vec<TaskHandle>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
|
||||
@@ -42,18 +42,6 @@ impl<Fut> ScopedFuture<Fut> {
|
||||
fut,
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps the given `Future` by taking the current [`Owner`] re-setting it as the
|
||||
/// active owner every time the inner `Future` is polled. Always untracks, i.e., clears
|
||||
/// the active [`Observer`] when polled.
|
||||
pub fn new_untracked(fut: Fut) -> Self {
|
||||
let owner = Owner::current().unwrap_or_default();
|
||||
Self {
|
||||
owner,
|
||||
observer: None,
|
||||
fut,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Fut: Future> Future for ScopedFuture<Fut> {
|
||||
|
||||
@@ -15,7 +15,7 @@ pub struct MemoInner<T, S>
|
||||
where
|
||||
S: Storage<T>,
|
||||
{
|
||||
/// Must always be acquired *after* the reactivity lock
|
||||
/// Must always be aquired *after* the reactivity lock
|
||||
pub(crate) value: Arc<RwLock<Option<S::Wrapped>>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub(crate) fun: Arc<dyn Fn(Option<T>) -> (T, bool) + Send + Sync>,
|
||||
@@ -137,7 +137,7 @@ where
|
||||
})
|
||||
});
|
||||
|
||||
// Two locks are acquired, so order matters.
|
||||
// Two locks are aquired, so order matters.
|
||||
let reactivity_lock = self.reactivity.write().or_poisoned();
|
||||
{
|
||||
// Safety: Can block endlessly if the user is has a ReadGuard on the value
|
||||
|
||||
@@ -27,7 +27,7 @@ use std::{fmt::Debug, hash::Hash, panic::Location};
|
||||
/// not re-run the calculation when a source signal changes until they are read again.
|
||||
///
|
||||
/// This is an arena-allocated type, which is `Copy` and is disposed when its reactive
|
||||
/// [`Owner`](crate::owner::Owner) cleans up. For a reference-counted signal that lives as
|
||||
/// [`Owner`](crate::owner::Owner) cleans up. For a reference-counted signal that livesas
|
||||
/// as long as a reference to it is alive, see [`ArcMemo`].
|
||||
///
|
||||
/// ```
|
||||
|
||||
@@ -14,7 +14,7 @@ pub struct SpecialNonReactiveZone;
|
||||
|
||||
/// Exits the "special non-reactive zone" when dropped.
|
||||
#[derive(Debug)]
|
||||
pub struct SpecialNonReactiveZoneGuard(bool);
|
||||
pub struct SpecialNonReactiveZoneGuard;
|
||||
|
||||
use pin_project_lite::pin_project;
|
||||
use std::{
|
||||
@@ -31,8 +31,8 @@ thread_local! {
|
||||
impl SpecialNonReactiveZone {
|
||||
/// Suppresses warnings about non-reactive accesses until the guard is dropped.
|
||||
pub fn enter() -> SpecialNonReactiveZoneGuard {
|
||||
let prev = IS_SPECIAL_ZONE.replace(true);
|
||||
SpecialNonReactiveZoneGuard(prev)
|
||||
IS_SPECIAL_ZONE.set(true);
|
||||
SpecialNonReactiveZoneGuard
|
||||
}
|
||||
|
||||
#[cfg(all(debug_assertions, feature = "effects"))]
|
||||
@@ -48,7 +48,7 @@ impl SpecialNonReactiveZone {
|
||||
|
||||
impl Drop for SpecialNonReactiveZoneGuard {
|
||||
fn drop(&mut self) {
|
||||
IS_SPECIAL_ZONE.set(self.0);
|
||||
IS_SPECIAL_ZONE.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -104,7 +104,15 @@ pub mod prelude {
|
||||
#[allow(unused)]
|
||||
#[doc(hidden)]
|
||||
pub fn log_warning(text: Arguments) {
|
||||
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
|
||||
#[cfg(feature = "tracing")]
|
||||
{
|
||||
tracing::warn!(text);
|
||||
}
|
||||
#[cfg(all(
|
||||
not(feature = "tracing"),
|
||||
target_arch = "wasm32",
|
||||
target_os = "unknown"
|
||||
))]
|
||||
{
|
||||
web_sys::console::warn_1(&text.to_string().into());
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! The reactive ownership model, which manages effect cancellation, cleanups, and arena allocation.
|
||||
//! The reactive ownership model, which manages effect cancelation, cleanups, and arena allocation.
|
||||
|
||||
#[cfg(feature = "hydration")]
|
||||
use hydration_context::SharedContext;
|
||||
@@ -32,7 +32,7 @@ pub use storage::*;
|
||||
pub use stored_value::{store_value, FromLocal, StoredValue};
|
||||
|
||||
/// A reactive owner, which manages
|
||||
/// 1) the cancellation of [`Effect`](crate::effect::Effect)s,
|
||||
/// 1) the cancelation of [`Effect`](crate::effect::Effect)s,
|
||||
/// 2) providing and accessing environment data via [`provide_context`] and [`use_context`],
|
||||
/// 3) running cleanup functions defined via [`Owner::on_cleanup`], and
|
||||
/// 4) an arena storage system to provide `Copy` handles via [`ArenaItem`], which is what allows
|
||||
@@ -167,7 +167,6 @@ impl Owner {
|
||||
.map(|parent| parent.read().or_poisoned().arena.clone())
|
||||
.unwrap_or_default(),
|
||||
paused: false,
|
||||
joined_owners: Vec::new(),
|
||||
})),
|
||||
#[cfg(feature = "hydration")]
|
||||
shared_context,
|
||||
@@ -202,7 +201,6 @@ impl Owner {
|
||||
#[cfg(feature = "sandboxed-arenas")]
|
||||
arena: Default::default(),
|
||||
paused: false,
|
||||
joined_owners: Vec::new(),
|
||||
})),
|
||||
#[cfg(feature = "hydration")]
|
||||
shared_context,
|
||||
@@ -228,7 +226,6 @@ impl Owner {
|
||||
#[cfg(feature = "sandboxed-arenas")]
|
||||
arena,
|
||||
paused,
|
||||
joined_owners: Vec::new(),
|
||||
})),
|
||||
#[cfg(feature = "hydration")]
|
||||
shared_context: self.shared_context.clone(),
|
||||
@@ -291,18 +288,12 @@ impl Owner {
|
||||
/// fill the same need as an "on unmount" function in other UI approaches, etc.
|
||||
pub fn on_cleanup(fun: impl FnOnce() + Send + Sync + 'static) {
|
||||
if let Some(owner) = Owner::current() {
|
||||
let mut inner = owner.inner.write().or_poisoned();
|
||||
|
||||
#[cfg(feature = "sandboxed-arenas")]
|
||||
let fun = {
|
||||
let arena = Arc::clone(&inner.arena);
|
||||
move || {
|
||||
Arena::set(&arena);
|
||||
fun()
|
||||
}
|
||||
};
|
||||
|
||||
inner.cleanups.push(Box::new(fun));
|
||||
owner
|
||||
.inner
|
||||
.write()
|
||||
.or_poisoned()
|
||||
.cleanups
|
||||
.push(Box::new(fun));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,7 +455,6 @@ pub(crate) struct OwnerInner {
|
||||
#[cfg(feature = "sandboxed-arenas")]
|
||||
arena: Arc<RwLock<ArenaMap>>,
|
||||
paused: bool,
|
||||
joined_owners: Vec<WeakOwner>,
|
||||
}
|
||||
|
||||
impl Debug for OwnerInner {
|
||||
|
||||
@@ -6,15 +6,6 @@ use std::{
|
||||
};
|
||||
|
||||
impl Owner {
|
||||
#[doc(hidden)]
|
||||
pub fn join_contexts(&self, other: &Owner) {
|
||||
self.inner
|
||||
.write()
|
||||
.or_poisoned()
|
||||
.joined_owners
|
||||
.push(other.downgrade());
|
||||
}
|
||||
|
||||
fn provide_context<T: Send + Sync + 'static>(&self, value: T) {
|
||||
self.inner
|
||||
.write()
|
||||
@@ -30,31 +21,22 @@ impl Owner {
|
||||
fn take_context<T: 'static>(&self) -> Option<T> {
|
||||
let ty = TypeId::of::<T>();
|
||||
let mut inner = self.inner.write().or_poisoned();
|
||||
let mut parent = inner.parent.as_ref().and_then(|p| p.upgrade());
|
||||
let contexts = &mut inner.contexts;
|
||||
if let Some(context) = contexts.remove(&ty) {
|
||||
context.downcast::<T>().ok().map(|n| *n)
|
||||
} else {
|
||||
let parent = inner.parent.as_ref().and_then(|p| p.upgrade());
|
||||
let joined = inner
|
||||
.joined_owners
|
||||
.iter()
|
||||
.flat_map(|owner| owner.upgrade().map(|owner| owner.inner));
|
||||
for parent in parent.into_iter().chain(joined) {
|
||||
let mut parent = Some(parent);
|
||||
while let Some(ref this_parent) = parent.clone() {
|
||||
let mut this_parent = this_parent.write().or_poisoned();
|
||||
let contexts = &mut this_parent.contexts;
|
||||
let value = contexts.remove(&ty);
|
||||
let downcast =
|
||||
value.and_then(|context| context.downcast::<T>().ok());
|
||||
if let Some(value) = downcast {
|
||||
return Some(*value);
|
||||
} else {
|
||||
parent = this_parent
|
||||
.parent
|
||||
.as_ref()
|
||||
.and_then(|p| p.upgrade());
|
||||
}
|
||||
while let Some(ref this_parent) = parent.clone() {
|
||||
let mut this_parent = this_parent.write().or_poisoned();
|
||||
let contexts = &mut this_parent.contexts;
|
||||
let value = contexts.remove(&ty);
|
||||
let downcast =
|
||||
value.and_then(|context| context.downcast::<T>().ok());
|
||||
if let Some(value) = downcast {
|
||||
return Some(*value);
|
||||
} else {
|
||||
parent =
|
||||
this_parent.parent.as_ref().and_then(|p| p.upgrade());
|
||||
}
|
||||
}
|
||||
None
|
||||
@@ -67,31 +49,22 @@ impl Owner {
|
||||
) -> Option<R> {
|
||||
let ty = TypeId::of::<T>();
|
||||
let inner = self.inner.read().or_poisoned();
|
||||
let mut parent = inner.parent.as_ref().and_then(|p| p.upgrade());
|
||||
let contexts = &inner.contexts;
|
||||
let reference = if let Some(context) = contexts.get(&ty) {
|
||||
context.downcast_ref::<T>()
|
||||
} else {
|
||||
let parent = inner.parent.as_ref().and_then(|p| p.upgrade());
|
||||
let joined = inner
|
||||
.joined_owners
|
||||
.iter()
|
||||
.flat_map(|owner| owner.upgrade().map(|owner| owner.inner));
|
||||
for parent in parent.into_iter().chain(joined) {
|
||||
let mut parent = Some(parent);
|
||||
while let Some(ref this_parent) = parent.clone() {
|
||||
let this_parent = this_parent.read().or_poisoned();
|
||||
let contexts = &this_parent.contexts;
|
||||
let value = contexts.get(&ty);
|
||||
let downcast =
|
||||
value.and_then(|context| context.downcast_ref::<T>());
|
||||
if let Some(value) = downcast {
|
||||
return Some(cb(value));
|
||||
} else {
|
||||
parent = this_parent
|
||||
.parent
|
||||
.as_ref()
|
||||
.and_then(|p| p.upgrade());
|
||||
}
|
||||
while let Some(ref this_parent) = parent.clone() {
|
||||
let this_parent = this_parent.read().or_poisoned();
|
||||
let contexts = &this_parent.contexts;
|
||||
let value = contexts.get(&ty);
|
||||
let downcast =
|
||||
value.and_then(|context| context.downcast_ref::<T>());
|
||||
if let Some(value) = downcast {
|
||||
return Some(cb(value));
|
||||
} else {
|
||||
parent =
|
||||
this_parent.parent.as_ref().and_then(|p| p.upgrade());
|
||||
}
|
||||
}
|
||||
None
|
||||
@@ -105,31 +78,22 @@ impl Owner {
|
||||
) -> Option<R> {
|
||||
let ty = TypeId::of::<T>();
|
||||
let mut inner = self.inner.write().or_poisoned();
|
||||
let mut parent = inner.parent.as_ref().and_then(|p| p.upgrade());
|
||||
let contexts = &mut inner.contexts;
|
||||
let reference = if let Some(context) = contexts.get_mut(&ty) {
|
||||
context.downcast_mut::<T>()
|
||||
} else {
|
||||
let parent = inner.parent.as_ref().and_then(|p| p.upgrade());
|
||||
let joined = inner
|
||||
.joined_owners
|
||||
.iter()
|
||||
.flat_map(|owner| owner.upgrade().map(|owner| owner.inner));
|
||||
for parent in parent.into_iter().chain(joined) {
|
||||
let mut parent = Some(parent);
|
||||
while let Some(ref this_parent) = parent.clone() {
|
||||
let mut this_parent = this_parent.write().or_poisoned();
|
||||
let contexts = &mut this_parent.contexts;
|
||||
let value = contexts.get_mut(&ty);
|
||||
let downcast =
|
||||
value.and_then(|context| context.downcast_mut::<T>());
|
||||
if let Some(value) = downcast {
|
||||
return Some(cb(value));
|
||||
} else {
|
||||
parent = this_parent
|
||||
.parent
|
||||
.as_ref()
|
||||
.and_then(|p| p.upgrade());
|
||||
}
|
||||
while let Some(ref this_parent) = parent.clone() {
|
||||
let mut this_parent = this_parent.write().or_poisoned();
|
||||
let contexts = &mut this_parent.contexts;
|
||||
let value = contexts.get_mut(&ty);
|
||||
let downcast =
|
||||
value.and_then(|context| context.downcast_mut::<T>());
|
||||
if let Some(value) = downcast {
|
||||
return Some(cb(value));
|
||||
} else {
|
||||
parent =
|
||||
this_parent.parent.as_ref().and_then(|p| p.upgrade());
|
||||
}
|
||||
}
|
||||
None
|
||||
@@ -272,11 +236,11 @@ pub fn provide_context<T: Send + Sync + 'static>(value: T) {
|
||||
///
|
||||
/// Effect::new(move |_| {
|
||||
/// // each use_context clones the value
|
||||
/// let value = use_context::<String>()
|
||||
/// .expect("could not find String in context");
|
||||
/// let value =
|
||||
/// use_context::<String>().expect("could not find i32 in context");
|
||||
/// assert_eq!(value, "foo");
|
||||
/// let value2 = use_context::<String>()
|
||||
/// .expect("could not find String in context");
|
||||
/// let value2 =
|
||||
/// use_context::<String>().expect("could not find i32 in context");
|
||||
/// assert_eq!(value2, "foo");
|
||||
/// });
|
||||
/// });
|
||||
@@ -320,11 +284,11 @@ pub fn use_context<T: Clone + 'static>() -> Option<T> {
|
||||
///
|
||||
/// Effect::new(move |_| {
|
||||
/// // each use_context clones the value
|
||||
/// let value = use_context::<String>()
|
||||
/// .expect("could not find String in context");
|
||||
/// let value =
|
||||
/// use_context::<String>().expect("could not find i32 in context");
|
||||
/// assert_eq!(value, "foo");
|
||||
/// let value2 = use_context::<String>()
|
||||
/// .expect("could not find String in context");
|
||||
/// let value2 =
|
||||
/// use_context::<String>().expect("could not find i32 in context");
|
||||
/// assert_eq!(value2, "foo");
|
||||
/// });
|
||||
/// });
|
||||
|
||||
@@ -9,7 +9,7 @@ use std::{
|
||||
/// An optional value that can always be sent between threads, even if its inner value
|
||||
/// in the `Some(_)` case would not be threadsafe.
|
||||
///
|
||||
/// This struct can be dereferenced to `Option<T>`.
|
||||
/// This struct can be derefenced to `Option<T>`.
|
||||
///
|
||||
/// If it has been given a local (`!Send`) value, that value is wrapped in a [`SendWrapper`], which
|
||||
/// allows sending it between threads but will panic if it is accessed or updated from a
|
||||
|
||||
@@ -22,11 +22,11 @@ use std::{
|
||||
/// allowing you to read or write directly to one of its field.
|
||||
///
|
||||
/// Tracking the mapped signal tracks changes to *any* part of the signal, and updating the signal notifies
|
||||
/// and notifies *all* dependencies of the signal. This is not a mechanism for fine-grained reactive updates
|
||||
/// and notifies *all* depenendencies of the signal. This is not a mechanism for fine-grained reactive updates
|
||||
/// to more complex data structures. Instead, it allows you to provide a signal-like API for wrapped types
|
||||
/// without exposing the original type directly to users.
|
||||
pub struct ArcMappedSignal<T> {
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: &'static Location<'static>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
try_read_untracked: Arc<
|
||||
@@ -44,7 +44,7 @@ pub struct ArcMappedSignal<T> {
|
||||
impl<T> Clone for ArcMappedSignal<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: self.defined_at,
|
||||
try_read_untracked: self.try_read_untracked.clone(),
|
||||
try_write: self.try_write.clone(),
|
||||
@@ -67,7 +67,7 @@ impl<T> ArcMappedSignal<T> {
|
||||
U: Send + Sync + 'static,
|
||||
{
|
||||
Self {
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
try_read_untracked: {
|
||||
let this = inner.clone();
|
||||
@@ -110,7 +110,7 @@ impl<T> ArcMappedSignal<T> {
|
||||
impl<T> Debug for ArcMappedSignal<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut partial = f.debug_struct("ArcMappedSignal");
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
#[cfg(debug_assertions)]
|
||||
partial.field("defined_at", &self.defined_at);
|
||||
partial.finish()
|
||||
}
|
||||
@@ -118,11 +118,11 @@ impl<T> Debug for ArcMappedSignal<T> {
|
||||
|
||||
impl<T> DefinedAt for ArcMappedSignal<T> {
|
||||
fn defined_at(&self) -> Option<&'static Location<'static>> {
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
Some(self.defined_at)
|
||||
}
|
||||
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
None
|
||||
}
|
||||
@@ -224,11 +224,11 @@ where
|
||||
/// allowing you to read or write directly to one of its field.
|
||||
///
|
||||
/// Tracking the mapped signal tracks changes to *any* part of the signal, and updating the signal notifies
|
||||
/// and notifies *all* dependencies of the signal. This is not a mechanism for fine-grained reactive updates
|
||||
/// and notifies *all* depenendencies of the signal. This is not a mechanism for fine-grained reactive updates
|
||||
/// to more complex data structures. Instead, it allows you to provide a signal-like API for wrapped types
|
||||
/// without exposing the original type directly to users.
|
||||
pub struct MappedSignal<T, S = SyncStorage> {
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: &'static Location<'static>,
|
||||
inner: StoredValue<ArcMappedSignal<T>, S>,
|
||||
}
|
||||
@@ -246,7 +246,7 @@ impl<T> MappedSignal<T> {
|
||||
U: Send + Sync + 'static,
|
||||
{
|
||||
Self {
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
inner: {
|
||||
let this = ArcRwSignal::from(inner);
|
||||
@@ -269,7 +269,7 @@ impl<T> Clone for MappedSignal<T> {
|
||||
impl<T> Debug for MappedSignal<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut partial = f.debug_struct("MappedSignal");
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
#[cfg(debug_assertions)]
|
||||
partial.field("defined_at", &self.defined_at);
|
||||
partial.finish()
|
||||
}
|
||||
@@ -277,11 +277,11 @@ impl<T> Debug for MappedSignal<T> {
|
||||
|
||||
impl<T> DefinedAt for MappedSignal<T> {
|
||||
fn defined_at(&self) -> Option<&'static Location<'static>> {
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
Some(self.defined_at)
|
||||
}
|
||||
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
None
|
||||
}
|
||||
@@ -352,7 +352,7 @@ where
|
||||
#[track_caller]
|
||||
fn from(value: ArcMappedSignal<T>) -> Self {
|
||||
MappedSignal {
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
inner: StoredValue::new(value),
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ use std::{hash::Hash, ops::DerefMut, panic::Location, sync::Arc};
|
||||
/// > Each of these has a related `_untracked()` method, which updates the signal
|
||||
/// > without notifying subscribers. Untracked updates are not desirable in most
|
||||
/// > cases, as they cause “tearing” between the signal’s value and its observed
|
||||
/// > value. If you want a non-reactive container, use [`ArenaItem`] instead.
|
||||
/// > value. If you want a non-reactive container, used [`ArenaItem`] instead.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```
|
||||
@@ -118,7 +118,7 @@ where
|
||||
#[track_caller]
|
||||
fn from(value: ArcWriteSignal<T>) -> Self {
|
||||
WriteSignal {
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
inner: ArenaItem::new_with_storage(value),
|
||||
}
|
||||
@@ -132,7 +132,7 @@ where
|
||||
#[track_caller]
|
||||
fn from_local(value: ArcWriteSignal<T>) -> Self {
|
||||
WriteSignal {
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
inner: ArenaItem::new_with_storage(value),
|
||||
}
|
||||
|
||||
@@ -643,7 +643,7 @@ pub trait IntoInner {
|
||||
/// The type of the value contained in the signal.
|
||||
type Value;
|
||||
|
||||
/// Returns the inner value if this is the only reference to the signal.
|
||||
/// Returns the inner value if this is the only reference to to the signal.
|
||||
/// Otherwise, returns `None` and drops this reference.
|
||||
/// # Panics
|
||||
/// Panics if the inner lock is poisoned.
|
||||
|
||||
@@ -11,8 +11,7 @@ pub mod read {
|
||||
},
|
||||
signal::{
|
||||
guards::{Mapped, Plain, ReadGuard},
|
||||
ArcMappedSignal, ArcReadSignal, ArcRwSignal, MappedSignal,
|
||||
ReadSignal, RwSignal,
|
||||
ArcReadSignal, ArcRwSignal, ReadSignal, RwSignal,
|
||||
},
|
||||
traits::{
|
||||
DefinedAt, Dispose, Get, Read, ReadUntracked, ReadValue, Track,
|
||||
@@ -808,16 +807,6 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<MappedSignal<T>> for Signal<T>
|
||||
where
|
||||
T: Clone + Send + Sync + 'static,
|
||||
{
|
||||
#[track_caller]
|
||||
fn from(value: MappedSignal<T>) -> Self {
|
||||
Self::derive(move || value.get())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<RwSignal<T, LocalStorage>> for Signal<T, LocalStorage>
|
||||
where
|
||||
T: 'static,
|
||||
@@ -850,16 +839,6 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<ArcMappedSignal<T>> for Signal<T>
|
||||
where
|
||||
T: Clone + Send + Sync + 'static,
|
||||
{
|
||||
#[track_caller]
|
||||
fn from(value: ArcMappedSignal<T>) -> Self {
|
||||
MappedSignal::from(value).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<ArcRwSignal<T>> for Signal<T, LocalStorage>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "reactive_stores"
|
||||
version = "0.2.2"
|
||||
version = "0.2.0-rc3"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -10,19 +10,19 @@ rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
guardian = { workspace = true, default-features = true }
|
||||
itertools = { workspace = true , default-features = true }
|
||||
guardian = "1.2"
|
||||
itertools = { workspace = true }
|
||||
or_poisoned = { workspace = true }
|
||||
paste = { workspace = true, default-features = true }
|
||||
paste = "1.0"
|
||||
reactive_graph = { workspace = true }
|
||||
rustc-hash = { workspace = true, default-features = true }
|
||||
rustc-hash = "2.0"
|
||||
reactive_stores_macro = { workspace = true }
|
||||
dashmap = { workspace = true, default-features = true }
|
||||
send_wrapper = { workspace = true, default-features = true }
|
||||
dashmap = "6.1"
|
||||
send_wrapper = "0.6.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { features = ["rt-multi-thread", "macros"] , workspace = true, default-features = true }
|
||||
tokio-test = { workspace = true, default-features = true }
|
||||
tokio = { version = "1.43", features = ["rt-multi-thread", "macros"] }
|
||||
tokio-test = { version = "0.4.4" }
|
||||
any_spawner = { workspace = true, features = ["futures-executor", "tokio"] }
|
||||
reactive_graph = { workspace = true, features = ["effects"] }
|
||||
leptos = { path = "../leptos", features = ["csr"] }
|
||||
|
||||
@@ -5,7 +5,7 @@ Stores are a data structure for nested reactivity.
|
||||
The [`reactive_graph`](https://crates.io/crates/reactive_graph) crate provides primitives for fine-grained reactivity
|
||||
via signals, memos, and effects.
|
||||
|
||||
This crate extends that reactivity to support reactive access to nested structs, without the need to create nested signals.
|
||||
This crate extends that reactivity to support reactive access to nested dested, without the need to create nested signals.
|
||||
|
||||
Using the `#[derive(Store)]` macro on a struct creates a series of getters that allow accessing each field. Individual fields
|
||||
can then be read as if they were signals. Changes to parents will notify their children, but changing one sibling field will
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user