mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-29 00:12:40 -05:00
Compare commits
28 Commits
actionform
...
keys
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2aa14e5e65 | ||
|
|
40f6ed9252 | ||
|
|
9d9ed45565 | ||
|
|
8e68699435 | ||
|
|
77580401da | ||
|
|
7902e7edb7 | ||
|
|
4ad223277d | ||
|
|
6f5da11c72 | ||
|
|
3eed86fbf3 | ||
|
|
10d51a854a | ||
|
|
6c60bad757 | ||
|
|
79f666b5da | ||
|
|
3ea3a40395 | ||
|
|
193aa79956 | ||
|
|
3481a6ee53 | ||
|
|
d1ef5fce9f | ||
|
|
5d48911f01 | ||
|
|
8a90f97959 | ||
|
|
e9665b34e5 | ||
|
|
d4b1ceda90 | ||
|
|
a0fae88f7d | ||
|
|
03a8609680 | ||
|
|
3e40f9cc66 | ||
|
|
576bb078f7 | ||
|
|
3cdcc85c87 | ||
|
|
ec3a26dfbc | ||
|
|
c755dae6ee | ||
|
|
b67d51e019 |
2
.github/workflows/check-examples.yml
vendored
2
.github/workflows/check-examples.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
|
||||
fail-fast: false
|
||||
uses: ./.github/workflows/run-example-task.yml
|
||||
uses: ./.github/workflows/run-cargo-make-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "check"
|
||||
|
||||
46
.github/workflows/check.yml
vendored
46
.github/workflows/check.yml
vendored
@@ -1,46 +0,0 @@
|
||||
name: Check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run `cargo check` ${{ matrix.os }} (using rustc ${{ matrix.rust }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
rust:
|
||||
- nightly
|
||||
os:
|
||||
- ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
components: rustfmt
|
||||
|
||||
- name: Add wasm32-unknown-unknown
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
|
||||
- name: Setup cargo-make
|
||||
uses: davidB/rust-cargo-make@v1
|
||||
|
||||
- name: Cargo generate-lockfile
|
||||
run: cargo generate-lockfile
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Run cargo check on all libraries
|
||||
run: cargo make --profile=github-actions check
|
||||
38
.github/workflows/ci.yml
vendored
Normal file
38
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
matrix-job:
|
||||
name: CI
|
||||
strategy:
|
||||
matrix:
|
||||
directory:
|
||||
[
|
||||
integrations/actix,
|
||||
integrations/axum,
|
||||
integrations/viz,
|
||||
integrations/utils,
|
||||
leptos,
|
||||
leptos_config,
|
||||
leptos_dom,
|
||||
leptos_hot_reload,
|
||||
leptos_macro,
|
||||
leptos_reactive,
|
||||
leptos_server,
|
||||
meta,
|
||||
router,
|
||||
server_fn,
|
||||
server_fn/server_fn_macro_default,
|
||||
server_fn_macro,
|
||||
]
|
||||
uses: ./.github/workflows/run-cargo-make-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "ci"
|
||||
34
.github/workflows/fmt.yml
vendored
34
.github/workflows/fmt.yml
vendored
@@ -1,34 +0,0 @@
|
||||
name: Format
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run rustfmt
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
rust:
|
||||
- nightly
|
||||
os:
|
||||
- ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
components: rustfmt
|
||||
|
||||
- name: Run Rustfmt
|
||||
run: cargo fmt -- --check
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Run Example Task
|
||||
name: Run Task
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
@@ -27,9 +27,9 @@ jobs:
|
||||
steps:
|
||||
# Setup environment
|
||||
- name: Install playwright browser dependencies
|
||||
run: |
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libegl1 libvpx7 libevent-2.1-7 libopus0 libopengl0 libwoff1 libharfbuzz-icu0 libgstreamer-plugins-base1.0-0 libgstreamer-gl1.0-0 libhyphen0 libmanette-0.2-0 libgles2 gstreamer1.0-libav
|
||||
sudo apt-get install libegl1 libvpx7 libevent-2.1-7 libopus0 libopengl0 libwoff1 libharfbuzz-icu0 libgstreamer-plugins-base1.0-0 libgstreamer-gl1.0-0 libhyphen0 libmanette-0.2-0 libgles2 gstreamer1.0-libav
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
@@ -84,7 +84,7 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
# Verify project
|
||||
# Run Cargo Make Task
|
||||
- name: ${{ inputs.cargo_make_task }}
|
||||
run: |
|
||||
if [ "${{ inputs.directory }}" = "INTERNAL" ]; then
|
||||
46
.github/workflows/test.yml
vendored
46
.github/workflows/test.yml
vendored
@@ -1,46 +0,0 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run tests ${{ matrix.os }} (using rustc ${{ matrix.rust }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
rust:
|
||||
- nightly
|
||||
os:
|
||||
- ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
components: rustfmt
|
||||
|
||||
- name: Add wasm32-unknown-unknown
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
|
||||
- name: Setup cargo-make
|
||||
uses: davidB/rust-cargo-make@v1
|
||||
|
||||
- name: Cargo generate-lockfile
|
||||
run: cargo generate-lockfile
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Run tests with all features
|
||||
run: cargo make --profile=github-actions test
|
||||
2
.github/workflows/verify-all-examples.yml
vendored
2
.github/workflows/verify-all-examples.yml
vendored
@@ -41,7 +41,7 @@ jobs:
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
|
||||
fail-fast: false
|
||||
uses: ./.github/workflows/run-example-task.yml
|
||||
uses: ./.github/workflows/run-cargo-make-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "verify-flow"
|
||||
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
|
||||
fail-fast: false
|
||||
uses: ./.github/workflows/run-example-task.yml
|
||||
uses: ./.github/workflows/run-cargo-make-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "verify-flow"
|
||||
|
||||
28
Cargo.toml
28
Cargo.toml
@@ -26,22 +26,22 @@ members = [
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.4.2"
|
||||
version = "0.4.3"
|
||||
|
||||
[workspace.dependencies]
|
||||
leptos = { path = "./leptos", version = "0.4.2" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.4.2" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.4.2" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.4.2" }
|
||||
leptos_reactive = { path = "./leptos_reactive", version = "0.4.2" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.4.2" }
|
||||
server_fn = { path = "./server_fn", version = "0.4.2" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.4.2" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.4.2" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.4.2" }
|
||||
leptos_router = { path = "./router", version = "0.4.2" }
|
||||
leptos_meta = { path = "./meta", version = "0.4.2" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.4.2" }
|
||||
leptos = { path = "./leptos", version = "0.4.3" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.4.3" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.4.3" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.4.3" }
|
||||
leptos_reactive = { path = "./leptos_reactive", version = "0.4.3" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.4.3" }
|
||||
server_fn = { path = "./server_fn", version = "0.4.3" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.4.3" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.4.3" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.4.3" }
|
||||
leptos_router = { path = "./router", version = "0.4.3" }
|
||||
leptos_meta = { path = "./meta", version = "0.4.3" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.4.3" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -3,113 +3,25 @@
|
||||
# cargo install --force cargo-make
|
||||
############
|
||||
|
||||
[config]
|
||||
# make tasks run at the workspace root
|
||||
default_to_workspace = false
|
||||
|
||||
[tasks.check]
|
||||
clear = true
|
||||
dependencies = [
|
||||
"check-all",
|
||||
"check-wasm",
|
||||
"check-all-release",
|
||||
"check-wasm-release",
|
||||
]
|
||||
|
||||
[tasks.check-all]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check-wasm]
|
||||
clear = true
|
||||
dependencies = [{ name = "check-wasm", path = "leptos" }]
|
||||
|
||||
[tasks.check-all-release]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check-wasm-release]
|
||||
clear = true
|
||||
dependencies = [{ name = "check-wasm-release", path = "leptos" }]
|
||||
|
||||
[tasks.check-examples]
|
||||
clear = true
|
||||
dependencies = [
|
||||
{ name = "check", path = "examples/counter" },
|
||||
{ name = "check", path = "examples/counter_isomorphic" },
|
||||
{ name = "check", path = "examples/counters" },
|
||||
{ name = "check", path = "examples/error_boundary" },
|
||||
{ name = "check", path = "examples/errors_axum" },
|
||||
{ name = "check", path = "examples/fetch" },
|
||||
{ name = "check", path = "examples/hackernews" },
|
||||
{ name = "check", path = "examples/hackernews_axum" },
|
||||
{ name = "check", path = "examples/js-framework-benchmark" },
|
||||
{ name = "check", path = "examples/leptos-tailwind-axum" },
|
||||
{ name = "check", path = "examples/login_with_token_csr_only" },
|
||||
{ name = "check", path = "examples/parent_child" },
|
||||
{ name = "check", path = "examples/router" },
|
||||
{ name = "check", path = "examples/session_auth_axum" },
|
||||
{ name = "check", path = "examples/slots" },
|
||||
{ name = "check", path = "examples/ssr_modes" },
|
||||
{ name = "check", path = "examples/ssr_modes_axum" },
|
||||
{ name = "check", path = "examples/tailwind" },
|
||||
{ name = "check", path = "examples/tailwind_csr_trunk" },
|
||||
{ name = "check", path = "examples/timer" },
|
||||
{ name = "check", path = "examples/todo_app_sqlite" },
|
||||
{ name = "check", path = "examples/todo_app_sqlite_axum" },
|
||||
{ name = "check", path = "examples/todo_app_sqlite_viz" },
|
||||
{ name = "check", path = "examples/todomvc" },
|
||||
]
|
||||
[env]
|
||||
CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
|
||||
[tasks.check-stable]
|
||||
workspace = false
|
||||
clear = true
|
||||
dependencies = [
|
||||
{ name = "check", path = "examples/counter_without_macros" },
|
||||
{ name = "check", path = "examples/counters_stable" },
|
||||
]
|
||||
|
||||
[tasks.test]
|
||||
clear = true
|
||||
dependencies = [
|
||||
"test-all",
|
||||
"test-leptos_macro-example",
|
||||
"doc-leptos_macro-example",
|
||||
]
|
||||
|
||||
[tasks.test-all]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "test-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.test-leptos_macro-example]
|
||||
description = "Tests the leptos_macro/example to check if macro handles doc comments correctly"
|
||||
command = "cargo"
|
||||
args = ["+nightly", "test", "--doc"]
|
||||
cwd = "leptos_macro/example"
|
||||
install_crate = false
|
||||
|
||||
[tasks.doc-leptos_macro-example]
|
||||
description = "Docs the leptos_macro/example to check if macro handles doc comments correctly"
|
||||
command = "cargo"
|
||||
args = ["+nightly", "doc"]
|
||||
cwd = "leptos_macro/example"
|
||||
install_crate = false
|
||||
|
||||
[tasks.ci-examples]
|
||||
workspace = false
|
||||
cwd = "examples"
|
||||
command = "cargo"
|
||||
args = ["make", "ci-clean"]
|
||||
|
||||
[tasks.clean-examples]
|
||||
workspace = false
|
||||
cwd = "examples"
|
||||
command = "cargo"
|
||||
args = ["make", "clean"]
|
||||
|
||||
[env]
|
||||
RUSTFLAGS = ""
|
||||
LEPTOS_OUTPUT_NAME = "ci" # allows examples to check/build without cargo-leptos
|
||||
|
||||
[env.github-actions]
|
||||
RUSTFLAGS = "-D warnings"
|
||||
|
||||
7
cargo-make/check.toml
Normal file
7
cargo-make/check.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[tasks.check]
|
||||
alias = "check-all"
|
||||
|
||||
[tasks.check-all]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
9
cargo-make/lint.toml
Normal file
9
cargo-make/lint.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[tasks.pre-clippy]
|
||||
env = { CARGO_MAKE_CLIPPY_ARGS = "--all-targets --all-features -- -D warnings" }
|
||||
|
||||
[tasks.check-style]
|
||||
dependencies = ["check-format-flow", "clippy-flow"]
|
||||
|
||||
[tasks.check-format]
|
||||
env = { LEPTOS_PROJECT_DIRECTORY = "../" }
|
||||
args = ["fmt", "--", "--check", "--config-path", "${LEPTOS_PROJECT_DIRECTORY}"]
|
||||
18
cargo-make/main.toml
Normal file
18
cargo-make/main.toml
Normal file
@@ -0,0 +1,18 @@
|
||||
extend = [
|
||||
{ path = "./check.toml" },
|
||||
{ path = "./lint.toml" },
|
||||
{ path = "./test.toml" },
|
||||
]
|
||||
|
||||
[env]
|
||||
RUSTFLAGS = ""
|
||||
LEPTOS_OUTPUT_NAME = "ci" # allows examples to check/build without cargo-leptos
|
||||
|
||||
[env.github-actions]
|
||||
RUSTFLAGS = "-D warnings"
|
||||
|
||||
[tasks.ci]
|
||||
dependencies = ["lint", "test"]
|
||||
|
||||
[tasks.lint]
|
||||
dependencies = ["check-format-flow"]
|
||||
7
cargo-make/test.toml
Normal file
7
cargo-make/test.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[tasks.test]
|
||||
alias = "test-all"
|
||||
|
||||
[tasks.test-all]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "test-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
2
docs/book/book.toml
Normal file
2
docs/book/book.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[output.html.playground]
|
||||
runnable = false
|
||||
@@ -14,31 +14,40 @@ If you don’t already have it installed, you can install Trunk by running
|
||||
cargo install trunk
|
||||
```
|
||||
|
||||
Create a basic Rust binary project
|
||||
Create a basic Rust project
|
||||
|
||||
```bash
|
||||
cargo init leptos-tutorial
|
||||
```
|
||||
|
||||
> We recommend using `nightly` Rust, as it enables [a few nice features](https://github.com/leptos-rs/leptos#nightly-note). To use `nightly` Rust with WebAssembly, you can run
|
||||
`cd` into your new `leptos-tutorial` project and add `leptos` as a dependency
|
||||
|
||||
```bash
|
||||
cargo add leptos --features=csr,nightly
|
||||
```
|
||||
|
||||
Or you can leave off `nighly` if you're using stable Rust
|
||||
```bash
|
||||
cargo add leptos --features=csr
|
||||
```
|
||||
|
||||
> Using `nightly` Rust, and the `nightly` feature in Leptos enables the function-call syntax for signal getters and setters that is used in most of this book.
|
||||
>
|
||||
> To use `nightly` Rust, you can run
|
||||
>
|
||||
> ```bash
|
||||
> rustup toolchain install nightly
|
||||
> rustup default nightly
|
||||
> ```
|
||||
>
|
||||
> If you’d rather use stable Rust with Leptos, you can do that too. In the guide and examples, you’ll just use the [`ReadSignal::get()`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html#impl-SignalGet%3CT%3E-for-ReadSignal%3CT%3E) and [`WriteSignal::set()`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html#impl-SignalGet%3CT%3E-for-ReadSignal%3CT%3E) methods instead of calling signal getters and setters as functions.
|
||||
|
||||
Make sure you've added the `wasm32-unknown-unknown` target do that Rust can compile your code to WebAssembly to run in the browser.
|
||||
Make sure you've added the `wasm32-unknown-unknown` target so that Rust can compile your code to WebAssembly to run in the browser.
|
||||
|
||||
```bash
|
||||
rustup target add wasm32-unknown-unknown
|
||||
```
|
||||
|
||||
`cd` into your new `leptos-tutorial` project and add `leptos` as a dependency
|
||||
|
||||
```bash
|
||||
cargo add leptos --features=csr,nightly # or just csr if you're using stable Rust
|
||||
```
|
||||
|
||||
Create a simple `index.html` in the root of the `leptos-tutorial` directory
|
||||
|
||||
```html
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
# Responding to Changes with create_effect
|
||||
@@ -23,7 +23,6 @@
|
||||
- [Transition](./async/12_transition.md)
|
||||
- [Actions](./async/13_actions.md)
|
||||
- [Interlude: Projecting Children](./interlude_projecting_children.md)
|
||||
- [Responding to Changes with `create_effect`](./14_create_effect.md)
|
||||
- [Global State Management](./15_global_state.md)
|
||||
- [Router](./router/README.md)
|
||||
- [Defining `<Routes/>`](./router/16_routes.md)
|
||||
|
||||
@@ -74,11 +74,11 @@ In other words, if this is being compiled to WASM, it has three items; otherwise
|
||||
When I load the page in the browser, I see nothing. If I open the console I see a bunch of warnings:
|
||||
|
||||
```
|
||||
element with id 0-0-1 not found, ignoring it for hydration
|
||||
element with id 0-0-2 not found, ignoring it for hydration
|
||||
element with id 0-0-3 not found, ignoring it for hydration
|
||||
component with id _0-0-4c not found, ignoring it for hydration
|
||||
component with id _0-0-4o not found, ignoring it for hydration
|
||||
element with id 0-3 not found, ignoring it for hydration
|
||||
element with id 0-4 not found, ignoring it for hydration
|
||||
element with id 0-5 not found, ignoring it for hydration
|
||||
component with id _0-6c not found, ignoring it for hydration
|
||||
component with id _0-6o not found, ignoring it for hydration
|
||||
```
|
||||
|
||||
The WASM version of your app, running in the browser, expects to find three items; but the HTML has none.
|
||||
@@ -87,6 +87,56 @@ The WASM version of your app, running in the browser, expects to find three item
|
||||
|
||||
It’s pretty rare that you do this intentionally, but it could happen from somehow running different logic on the server and in the browser. If you’re seeing warnings like this and you don’t think it’s your fault, it’s much more likely that it’s a bug with `<Suspense/>` or something. Feel free to go ahead and open an [issue](https://github.com/leptos-rs/leptos/issues) or [discussion](https://github.com/leptos-rs/leptos/discussions) on GitHub for help.
|
||||
|
||||
### Mutating the DOM during rendering
|
||||
|
||||
This is a slightly more common way to create a client/server mismatch: updating a signal _during rendering_ in a way that mutates the view.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
let (loaded, set_loaded) = create_signal(cx, false);
|
||||
|
||||
// create_effect only runs on the client
|
||||
create_effect(cx, move |_| {
|
||||
// do something like reading from localStorage
|
||||
set_loaded(true);
|
||||
});
|
||||
|
||||
move || {
|
||||
if loaded() {
|
||||
view! { cx, <p>"Hello, world!"</p> }.into_any()
|
||||
} else {
|
||||
view! { cx, <div class="loading">"Loading..."</div> }.into_any()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This one gives us the scary panic
|
||||
|
||||
```
|
||||
panicked at 'assertion failed: `(left == right)`
|
||||
left: `"DIV"`,
|
||||
right: `"P"`: SSR and CSR elements have the same hydration key but different node kinds.
|
||||
```
|
||||
|
||||
And a handy link to this page!
|
||||
|
||||
The problem here is that `create_effect` runs **immediately** and **synchronously**, but only in the browser. As a result, on the server, `loaded` is false, and a `<div>` is rendered. But on the browser, by the time the view is being rendered, `loaded` has already been set to `true`, and the browser is expecting to find a `<p>`.
|
||||
|
||||
#### Solution
|
||||
|
||||
You can simply tell the effect to wait a tick before updating the signal, by using something like `request_animation_frame`, which will set a short timeout and then update the signal before the next frame.
|
||||
|
||||
```rust
|
||||
create_effect(cx, move |_| {
|
||||
// do something like reading from localStorage
|
||||
request_animation_frame(move || set_loaded(true));
|
||||
});
|
||||
```
|
||||
|
||||
This allows the browser to hydrate with the correct, matching state (`loaded` is `false` when it reaches the view), then immediately update it to `true` once hydration is complete.
|
||||
|
||||
### Not all client code can run on the server
|
||||
|
||||
Imagine you happily import a dependency like `gloo-net` that you’ve been used to using to make requests in the browser, and use it in a `create_resource` in a server-rendered app.
|
||||
|
||||
@@ -4,6 +4,14 @@ use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
|
||||
pub struct Todo {
|
||||
id: u16,
|
||||
title: String,
|
||||
completed: bool,
|
||||
}
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use sqlx::{Connection, SqliteConnection};
|
||||
@@ -11,20 +19,6 @@ cfg_if! {
|
||||
pub async fn db() -> Result<SqliteConnection, ServerFnError> {
|
||||
Ok(SqliteConnection::connect("sqlite:Todos.db").await?)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
|
||||
pub struct Todo {
|
||||
id: u16,
|
||||
title: String,
|
||||
completed: bool,
|
||||
}
|
||||
} else {
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Todo {
|
||||
id: u16,
|
||||
title: String,
|
||||
completed: bool,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,14 @@ use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
|
||||
pub struct Todo {
|
||||
id: u16,
|
||||
title: String,
|
||||
completed: bool,
|
||||
}
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use sqlx::{Connection, SqliteConnection};
|
||||
@@ -13,20 +21,6 @@ cfg_if! {
|
||||
pub async fn db() -> Result<SqliteConnection, ServerFnError> {
|
||||
Ok(SqliteConnection::connect("sqlite:Todos.db").await?)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
|
||||
pub struct Todo {
|
||||
id: u16,
|
||||
title: String,
|
||||
completed: bool,
|
||||
}
|
||||
} else {
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Todo {
|
||||
id: u16,
|
||||
title: String,
|
||||
completed: bool,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,14 @@ use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
|
||||
pub struct Todo {
|
||||
id: u16,
|
||||
title: String,
|
||||
completed: bool,
|
||||
}
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use sqlx::{Connection, SqliteConnection};
|
||||
@@ -13,20 +21,6 @@ cfg_if! {
|
||||
pub async fn db() -> Result<SqliteConnection, ServerFnError> {
|
||||
Ok(SqliteConnection::connect("sqlite:Todos.db").await?)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
|
||||
pub struct Todo {
|
||||
id: u16,
|
||||
title: String,
|
||||
completed: bool,
|
||||
}
|
||||
} else {
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Todo {
|
||||
id: u16,
|
||||
title: String,
|
||||
completed: bool,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,3 +19,6 @@ serde_json = "1"
|
||||
parking_lot = "0.12.1"
|
||||
regex = "1.7.0"
|
||||
tracing = "0.1.37"
|
||||
|
||||
[features]
|
||||
nonce = ["leptos/nonce"]
|
||||
|
||||
4
integrations/actix/Makefile.toml
Normal file
4
integrations/actix/Makefile.toml
Normal file
@@ -0,0 +1,4 @@
|
||||
extend = { path = "../../cargo-make/main.toml" }
|
||||
|
||||
[tasks.check-format]
|
||||
env = { LEPTOS_PROJECT_DIRECTORY = "../../" }
|
||||
@@ -223,26 +223,30 @@ pub fn handle_server_fns_with_context(
|
||||
let res_options =
|
||||
use_context::<ResponseOptions>(cx).unwrap();
|
||||
|
||||
let mut res: HttpResponseBuilder;
|
||||
let mut res: HttpResponseBuilder =
|
||||
HttpResponse::Ok();
|
||||
let res_parts = res_options.0.write();
|
||||
|
||||
if accept_header == Some("application/json")
|
||||
|| accept_header
|
||||
== Some("application/x-www-form-urlencoded")
|
||||
|| accept_header == Some("application/cbor")
|
||||
// if accept_header isn't set to one of these, it's a form submit
|
||||
// redirect back to the referrer if not redirect has been set
|
||||
if accept_header != Some("application/json")
|
||||
&& accept_header
|
||||
!= Some("application/x-www-form-urlencoded")
|
||||
&& accept_header != Some("application/cbor")
|
||||
{
|
||||
res = HttpResponse::Ok();
|
||||
}
|
||||
// otherwise, it's probably a <form> submit or something: redirect back to the referrer
|
||||
else {
|
||||
let referer = req
|
||||
.headers()
|
||||
.get("Referer")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.unwrap_or("/");
|
||||
res = HttpResponse::SeeOther();
|
||||
res.insert_header(("Location", referer))
|
||||
.content_type("application/json");
|
||||
// Location will already be set if redirect() has been used
|
||||
let has_location_set =
|
||||
res_parts.headers.get("Location").is_some();
|
||||
if !has_location_set {
|
||||
let referer = req
|
||||
.headers()
|
||||
.get("Referer")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.unwrap_or("/");
|
||||
res = HttpResponse::SeeOther();
|
||||
res.insert_header(("Location", referer))
|
||||
.content_type("application/json");
|
||||
}
|
||||
};
|
||||
// Override StatusCode if it was set in a Resource or Element
|
||||
if let Some(status) = res_parts.status {
|
||||
@@ -288,7 +292,12 @@ pub fn handle_server_fns_with_context(
|
||||
} else {
|
||||
HttpResponse::BadRequest().body(format!(
|
||||
"Could not find a server function at the route {:?}. \
|
||||
\n\nIt's likely that you need to call \
|
||||
\n\nIt's likely that either
|
||||
1. The API prefix you specify in the `#[server]` \
|
||||
macro doesn't match the prefix at which your server \
|
||||
function handler is mounted, or \n2. You are on a \
|
||||
platform that doesn't support automatic server \
|
||||
function registration and you need to call \
|
||||
ServerFn::register_explicit() on the server function \
|
||||
type, somewhere in your `main` function.",
|
||||
req.path()
|
||||
@@ -724,6 +733,8 @@ fn provide_contexts(
|
||||
provide_context(cx, res_options);
|
||||
provide_context(cx, req.clone());
|
||||
provide_server_redirect(cx, move |path| redirect(cx, path));
|
||||
#[cfg(feature = "nonce")]
|
||||
leptos::nonce::provide_nonce(cx);
|
||||
}
|
||||
|
||||
fn leptos_corrected_path(req: &HttpRequest) -> String {
|
||||
@@ -788,8 +799,11 @@ async fn build_stream_response(
|
||||
// wait for any blocking resources to load before pulling metadata
|
||||
let first_app_chunk = stream.next().await.unwrap_or_default();
|
||||
|
||||
let (head, tail) =
|
||||
html_parts_separated(options, use_context::<MetaContext>(cx).as_ref());
|
||||
let (head, tail) = html_parts_separated(
|
||||
cx,
|
||||
options,
|
||||
use_context::<MetaContext>(cx).as_ref(),
|
||||
);
|
||||
|
||||
let mut stream = Box::pin(
|
||||
futures::stream::once(async move { head.clone() })
|
||||
|
||||
@@ -19,6 +19,9 @@ leptos_integration_utils = { workspace = true }
|
||||
serde_json = "1"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
parking_lot = "0.12.1"
|
||||
tokio-util = {version = "0.7.7", features = ["rt"] }
|
||||
tokio-util = { version = "0.7.7", features = ["rt"] }
|
||||
tracing = "0.1.37"
|
||||
once_cell = "1.17"
|
||||
|
||||
[features]
|
||||
nonce = ["leptos/nonce"]
|
||||
|
||||
4
integrations/axum/Makefile.toml
Normal file
4
integrations/axum/Makefile.toml
Normal file
@@ -0,0 +1,4 @@
|
||||
extend = { path = "../../cargo-make/main.toml" }
|
||||
|
||||
[tasks.check-format]
|
||||
env = { LEPTOS_PROJECT_DIRECTORY = "../../" }
|
||||
@@ -335,9 +335,14 @@ async fn handle_server_fns_inner(
|
||||
Response::builder().status(StatusCode::BAD_REQUEST).body(
|
||||
Full::from(format!(
|
||||
"Could not find a server function at the route \
|
||||
{fn_name}. \n\nIt's likely that you need to call \
|
||||
{fn_name}. \n\nIt's likely that either
|
||||
1. The API prefix you specify in the `#[server]` \
|
||||
macro doesn't match the prefix at which your server \
|
||||
function handler is mounted, or \n2. You are on a \
|
||||
platform that doesn't support automatic server \
|
||||
function registration and you need to call \
|
||||
ServerFn::register_explicit() on the server function \
|
||||
type, somewhere in your `main` function."
|
||||
type, somewhere in your `main` function.",
|
||||
)),
|
||||
)
|
||||
}
|
||||
@@ -688,8 +693,11 @@ async fn forward_stream(
|
||||
let mut shell = Box::pin(bundle);
|
||||
let first_app_chunk = shell.next().await.unwrap_or_default();
|
||||
|
||||
let (head, tail) =
|
||||
html_parts_separated(options, use_context::<MetaContext>(cx).as_ref());
|
||||
let (head, tail) = html_parts_separated(
|
||||
cx,
|
||||
options,
|
||||
use_context::<MetaContext>(cx).as_ref(),
|
||||
);
|
||||
|
||||
_ = tx.send(head).await;
|
||||
_ = tx.send(first_app_chunk).await;
|
||||
@@ -823,6 +831,8 @@ fn provide_contexts(
|
||||
provide_context(cx, extractor);
|
||||
provide_context(cx, default_res_options);
|
||||
provide_server_redirect(cx, move |path| redirect(cx, path));
|
||||
#[cfg(feature = "nonce")]
|
||||
leptos::nonce::provide_nonce(cx);
|
||||
}
|
||||
|
||||
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
|
||||
|
||||
4
integrations/utils/Makefile.toml
Normal file
4
integrations/utils/Makefile.toml
Normal file
@@ -0,0 +1,4 @@
|
||||
extend = { path = "../../cargo-make/main.toml" }
|
||||
|
||||
[tasks.check-format]
|
||||
env = { LEPTOS_PROJECT_DIRECTORY = "../../" }
|
||||
@@ -1,18 +1,18 @@
|
||||
use futures::{Stream, StreamExt};
|
||||
use leptos::{use_context, RuntimeId, ScopeId};
|
||||
use leptos::{nonce::use_nonce, use_context, RuntimeId, Scope, ScopeId};
|
||||
use leptos_config::LeptosOptions;
|
||||
use leptos_meta::MetaContext;
|
||||
|
||||
extern crate tracing;
|
||||
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
fn autoreload(options: &LeptosOptions) -> String {
|
||||
fn autoreload(nonce_str: &str, options: &LeptosOptions) -> String {
|
||||
let site_ip = &options.site_addr.ip().to_string();
|
||||
let reload_port = options.reload_port;
|
||||
match std::env::var("LEPTOS_WATCH").is_ok() {
|
||||
true => format!(
|
||||
r#"
|
||||
<script crossorigin="">(function () {{
|
||||
<script crossorigin=""{nonce_str}>(function () {{
|
||||
{}
|
||||
var ws = new WebSocket('ws://{site_ip}:{reload_port}/live_reload');
|
||||
ws.onmessage = (ev) => {{
|
||||
@@ -42,6 +42,8 @@ fn autoreload(options: &LeptosOptions) -> String {
|
||||
false => "".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[deprecated = "Use html_parts_separated."]
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
pub fn html_parts(
|
||||
options: &LeptosOptions,
|
||||
@@ -58,7 +60,7 @@ pub fn html_parts(
|
||||
wasm_output_name.push_str("_bg");
|
||||
}
|
||||
|
||||
let leptos_autoreload = autoreload(options);
|
||||
let leptos_autoreload = autoreload("".into(), options);
|
||||
|
||||
let html_metadata =
|
||||
meta.and_then(|mc| mc.html.as_string()).unwrap_or_default();
|
||||
@@ -80,21 +82,27 @@ pub fn html_parts(
|
||||
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
pub fn html_parts_separated(
|
||||
cx: Scope,
|
||||
options: &LeptosOptions,
|
||||
meta: Option<&MetaContext>,
|
||||
) -> (String, &'static str) {
|
||||
let pkg_path = &options.site_pkg_dir;
|
||||
let output_name = &options.output_name;
|
||||
let nonce = use_nonce(cx);
|
||||
let nonce = nonce
|
||||
.as_ref()
|
||||
.map(|nonce| format!(" nonce=\"{nonce}\""))
|
||||
.unwrap_or_default();
|
||||
|
||||
// Because wasm-pack adds _bg to the end of the WASM filename, and we want to mantain compatibility with it's default options
|
||||
// we add _bg to the wasm files if cargo-leptos doesn't set the env var LEPTOS_OUTPUT_NAME
|
||||
// Otherwise we need to add _bg because wasm_pack always does. This is not the same as options.output_name, which is set regardless
|
||||
// we add _bg to the wasm files if cargo-leptos doesn't set the env var LEPTOS_OUTPUT_NAME at compile time
|
||||
// Otherwise we need to add _bg because wasm_pack always does.
|
||||
let mut wasm_output_name = output_name.clone();
|
||||
if std::env::var("LEPTOS_OUTPUT_NAME").is_err() {
|
||||
if std::option_env!("LEPTOS_OUTPUT_NAME").is_none() {
|
||||
wasm_output_name.push_str("_bg");
|
||||
}
|
||||
|
||||
let leptos_autoreload = autoreload(options);
|
||||
let leptos_autoreload = autoreload(&nonce, options);
|
||||
|
||||
let html_metadata =
|
||||
meta.and_then(|mc| mc.html.as_string()).unwrap_or_default();
|
||||
@@ -109,9 +117,9 @@ pub fn html_parts_separated(
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
{head}
|
||||
<link rel="modulepreload" href="/{pkg_path}/{output_name}.js">
|
||||
<link rel="preload" href="/{pkg_path}/{wasm_output_name}.wasm" as="fetch" type="application/wasm" crossorigin="">
|
||||
<script type="module">import init, {{ hydrate }} from '/{pkg_path}/{output_name}.js'; init('/{pkg_path}/{wasm_output_name}.wasm').then(hydrate);</script>
|
||||
<link rel="modulepreload" href="/{pkg_path}/{output_name}.js"{nonce}>
|
||||
<link rel="preload" href="/{pkg_path}/{wasm_output_name}.wasm" as="fetch" type="application/wasm" crossorigin=""{nonce}>
|
||||
<script type="module"{nonce}>import init, {{ hydrate }} from '/{pkg_path}/{output_name}.js'; init('/{pkg_path}/{wasm_output_name}.wasm').then(hydrate);</script>
|
||||
{leptos_autoreload}
|
||||
"#
|
||||
);
|
||||
@@ -133,15 +141,14 @@ pub async fn build_async_response(
|
||||
}
|
||||
|
||||
let cx = leptos::Scope { runtime, id: scope };
|
||||
let (head, tail) =
|
||||
html_parts_separated(options, use_context::<MetaContext>(cx).as_ref());
|
||||
let (head, tail) = html_parts_separated(
|
||||
cx,
|
||||
options,
|
||||
use_context::<MetaContext>(cx).as_ref(),
|
||||
);
|
||||
|
||||
// in async, we load the meta content *now*, after the suspenses have resolved
|
||||
let meta = use_context::<MetaContext>(cx);
|
||||
let head_meta = meta
|
||||
.as_ref()
|
||||
.map(|meta| meta.dehydrate())
|
||||
.unwrap_or_default();
|
||||
let body_meta = meta
|
||||
.as_ref()
|
||||
.and_then(|meta| meta.body.as_string())
|
||||
@@ -149,5 +156,5 @@ pub async fn build_async_response(
|
||||
|
||||
runtime.dispose();
|
||||
|
||||
format!("{head}{head_meta}</head><body{body_meta}>{buf}{tail}")
|
||||
format!("{head}</head><body{body_meta}>{buf}{tail}")
|
||||
}
|
||||
|
||||
@@ -19,3 +19,6 @@ leptos_integration_utils = { workspace = true }
|
||||
serde_json = "1"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
parking_lot = "0.12.1"
|
||||
|
||||
[features]
|
||||
nonce = ["leptos/nonce"]
|
||||
|
||||
4
integrations/viz/Makefile.toml
Normal file
4
integrations/viz/Makefile.toml
Normal file
@@ -0,0 +1,4 @@
|
||||
extend = { path = "../../cargo-make/main.toml" }
|
||||
|
||||
[tasks.check-format]
|
||||
env = { LEPTOS_PROJECT_DIRECTORY = "../../" }
|
||||
@@ -312,10 +312,17 @@ async fn handle_server_fns_inner(
|
||||
.body(Body::from(format!(
|
||||
"Could not find a server function at the \
|
||||
route {fn_name}. \n\nIt's likely that \
|
||||
you need to call \
|
||||
either
|
||||
1. The API prefix you specify in the \
|
||||
`#[server]` macro doesn't match the \
|
||||
prefix at which your server function \
|
||||
handler is mounted, or \n2. You are on a \
|
||||
platform that doesn't support automatic \
|
||||
server function registration and you \
|
||||
need to call \
|
||||
ServerFn::register_explicit() on the \
|
||||
server function type, somewhere in your \
|
||||
`main` function."
|
||||
`main` function.",
|
||||
)))
|
||||
}
|
||||
.expect("could not build Response");
|
||||
@@ -648,8 +655,11 @@ async fn forward_stream(
|
||||
mut tx: Sender<String>,
|
||||
) {
|
||||
let cx = Scope { runtime, id: scope };
|
||||
let (head, tail) =
|
||||
html_parts_separated(options, use_context::<MetaContext>(cx).as_ref());
|
||||
let (head, tail) = html_parts_separated(
|
||||
cx,
|
||||
options,
|
||||
use_context::<MetaContext>(cx).as_ref(),
|
||||
);
|
||||
|
||||
_ = tx.send(head).await;
|
||||
let mut shell = Box::pin(bundle);
|
||||
@@ -786,6 +796,8 @@ fn provide_contexts(
|
||||
provide_context(cx, req_parts);
|
||||
provide_context(cx, default_res_options);
|
||||
provide_server_redirect(cx, move |path| redirect(cx, path));
|
||||
#[cfg(feature = "nonce")]
|
||||
leptos::nonce::provide_nonce(cx);
|
||||
}
|
||||
|
||||
/// Returns a Viz [Handler](viz::Handler) that listens for a `GET` request and tries
|
||||
|
||||
@@ -13,16 +13,16 @@ cfg-if = "1"
|
||||
leptos_dom = { workspace = true }
|
||||
leptos_macro = { workspace = true }
|
||||
leptos_reactive = { workspace = true }
|
||||
leptos_server = { workspace = true}
|
||||
leptos_server = { workspace = true }
|
||||
leptos_config = { workspace = true }
|
||||
tracing = "0.1"
|
||||
typed-builder = "0.14"
|
||||
server_fn = { workspace = true}
|
||||
server_fn = { workspace = true }
|
||||
web-sys = { version = "0.3.63", optional = true }
|
||||
wasm-bindgen = { version = "0.2", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
leptos = { path = "."}
|
||||
leptos = { path = "." }
|
||||
|
||||
[features]
|
||||
default = ["serde"]
|
||||
@@ -58,9 +58,18 @@ serde-lite = ["leptos_reactive/serde-lite"]
|
||||
miniserde = ["leptos_reactive/miniserde"]
|
||||
rkyv = ["leptos_reactive/rkyv"]
|
||||
tracing = ["leptos_macro/tracing"]
|
||||
nonce = ["leptos_dom/nonce"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["nightly", "tracing", "template_macro", "rustls", "default-tls", "web-sys", "wasm-bindgen"]
|
||||
denylist = [
|
||||
"nightly",
|
||||
"tracing",
|
||||
"template_macro",
|
||||
"rustls",
|
||||
"default-tls",
|
||||
"web-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
skip_feature_sets = [
|
||||
[
|
||||
"csr",
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
extend = { path = "../cargo-make/main.toml" }
|
||||
|
||||
[tasks.check]
|
||||
clear = true
|
||||
dependencies = [
|
||||
"check-all",
|
||||
"check-wasm",
|
||||
"check-release",
|
||||
"check-wasm-release",
|
||||
]
|
||||
|
||||
[tasks.check-wasm]
|
||||
clear = true
|
||||
dependencies = ["check-hydrate", "check-csr"]
|
||||
@@ -43,3 +54,7 @@ args = [
|
||||
"--features=csr",
|
||||
"--target=wasm32-unknown-unknown",
|
||||
]
|
||||
|
||||
[tasks.check-release]
|
||||
command = "cargo"
|
||||
args = ["check", "--release"]
|
||||
|
||||
@@ -58,13 +58,15 @@ where
|
||||
F: Fn(Scope, RwSignal<Errors>) -> IV + 'static,
|
||||
IV: IntoView,
|
||||
{
|
||||
_ = HydrationCtx::next_component();
|
||||
let before_children = HydrationCtx::next_component("e");
|
||||
|
||||
let errors: RwSignal<Errors> = create_rw_signal(cx, Errors::default());
|
||||
|
||||
provide_context(cx, errors);
|
||||
|
||||
// Run children so that they render and execute resources
|
||||
let children = children(cx);
|
||||
HydrationCtx::continue_from(before_children);
|
||||
|
||||
#[cfg(all(debug_assertions, feature = "hydrate"))]
|
||||
{
|
||||
|
||||
@@ -84,11 +84,18 @@
|
||||
//! from the server to the client.
|
||||
//! - `serde-lite` In SSR/hydrate mode, uses [serde-lite](https://docs.rs/serde-lite/latest/serde_lite/) to serialize resources and send them
|
||||
//! from the server to the client.
|
||||
//! - `rkyv` In SSR/hydrate mode, uses [rkyv](https://docs.rs/rkyv/latest/rkyv/) to serialize resources and send them
|
||||
//! from the server to the client.
|
||||
//! - `miniserde` In SSR/hydrate mode, uses [miniserde](https://docs.rs/miniserde/latest/miniserde/) to serialize resources and send them
|
||||
//! from the server to the client.
|
||||
//! - `tracing` Adds additional support for [`tracing`](https://docs.rs/tracing/latest/tracing/) to components.
|
||||
//! - `default-tls` Use default native TLS support. (Only applies when using server functions with a non-WASM client like a desktop app.)
|
||||
//! - `rustls` Use `rustls`. (Only applies when using server functions with a non-WASM client like a desktop app.)
|
||||
//! - `template_macro` Enables the [`template!`](leptos_macro::template) macro, which offers faster DOM node creation for some use cases in `csr`.
|
||||
//!
|
||||
//! **Important Note:** You must enable one of `csr`, `hydrate`, or `ssr` to tell Leptos
|
||||
//! which mode your app is operating in.
|
||||
//! which mode your app is operating in. You should only enable one of these per build target,
|
||||
//! i.e., you should not have both `hydrate` and `ssr` enabled for your server binary, only `ssr`.
|
||||
//!
|
||||
//! # A Simple Counter
|
||||
//!
|
||||
@@ -157,9 +164,10 @@ pub use leptos_dom::{
|
||||
set_interval_with_handle, set_timeout, set_timeout_with_handle,
|
||||
window_event_listener, window_event_listener_untyped,
|
||||
},
|
||||
html, log, math, mount_to, mount_to_body, svg, warn, window, Attribute,
|
||||
Class, CollectView, Errors, Fragment, HtmlElement, IntoAttribute,
|
||||
IntoClass, IntoProperty, IntoStyle, IntoView, NodeRef, Property, View,
|
||||
html, log, math, mount_to, mount_to_body, nonce, svg, warn, window,
|
||||
Attribute, Class, CollectView, Errors, Fragment, HtmlElement,
|
||||
IntoAttribute, IntoClass, IntoProperty, IntoStyle, IntoView, NodeRef,
|
||||
Property, View,
|
||||
};
|
||||
|
||||
/// Types to make it easier to handle errors in your application.
|
||||
|
||||
@@ -69,11 +69,13 @@ where
|
||||
// provide this SuspenseContext to any resources below it
|
||||
provide_context(cx, context);
|
||||
|
||||
let current_id = HydrationCtx::next_component();
|
||||
let before = HydrationCtx::peek();
|
||||
let current_id = HydrationCtx::next_component("s");
|
||||
leptos::log!("<Suspense/> next_component {current_id}");
|
||||
|
||||
let child = DynChild::new({
|
||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
||||
let current_id = current_id;
|
||||
let current_id = current_id.clone();
|
||||
|
||||
let children = Rc::new(orig_children(cx).into_view(cx));
|
||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
||||
@@ -97,7 +99,7 @@ where
|
||||
{
|
||||
// no resources were read under this, so just return the child
|
||||
if context.pending_resources.get() == 0 {
|
||||
HydrationCtx::continue_from(current_id);
|
||||
HydrationCtx::continue_from(current_id.clone());
|
||||
DynChild::new({
|
||||
let children = Rc::clone(&children);
|
||||
move || (*children).clone()
|
||||
@@ -106,7 +108,7 @@ where
|
||||
}
|
||||
// show the fallback, but also prepare to stream HTML
|
||||
else {
|
||||
HydrationCtx::continue_from(current_id);
|
||||
HydrationCtx::continue_from(current_id.clone());
|
||||
|
||||
cx.register_suspense(
|
||||
context,
|
||||
@@ -114,8 +116,11 @@ where
|
||||
// out-of-order streaming
|
||||
{
|
||||
let orig_children = Rc::clone(&orig_children);
|
||||
let current_id = current_id.clone();
|
||||
move || {
|
||||
HydrationCtx::continue_from(current_id);
|
||||
HydrationCtx::continue_from(
|
||||
current_id.clone(),
|
||||
);
|
||||
DynChild::new({
|
||||
let orig_children =
|
||||
orig_children(cx).into_view(cx);
|
||||
@@ -129,8 +134,11 @@ where
|
||||
// in-order streaming
|
||||
{
|
||||
let orig_children = Rc::clone(&orig_children);
|
||||
let current_id = current_id.clone();
|
||||
move || {
|
||||
HydrationCtx::continue_from(current_id);
|
||||
HydrationCtx::continue_from(
|
||||
current_id.clone(),
|
||||
);
|
||||
DynChild::new({
|
||||
let orig_children =
|
||||
orig_children(cx).into_view(cx);
|
||||
@@ -155,8 +163,8 @@ where
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
HydrationCtx::continue_from(current_id);
|
||||
HydrationCtx::next_component();
|
||||
HydrationCtx::continue_from(before.clone());
|
||||
_ = HydrationCtx::id();
|
||||
|
||||
leptos_dom::View::Suspense(current_id, core_component)
|
||||
}
|
||||
|
||||
@@ -289,4 +289,4 @@ fn None(cx: Scope) -> impl IntoView {
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1
leptos_config/Makefile.toml
Normal file
1
leptos_config/Makefile.toml
Normal file
@@ -0,0 +1 @@
|
||||
extend = { path = "../cargo-make/main.toml" }
|
||||
@@ -14,6 +14,7 @@ use typed_builder::TypedBuilder;
|
||||
/// A Struct to allow us to parse LeptosOptions from the file. Not really needed, most interactions should
|
||||
/// occur with LeptosOptions
|
||||
#[derive(Clone, Debug, serde::Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct ConfFile {
|
||||
pub leptos_options: LeptosOptions,
|
||||
}
|
||||
@@ -23,31 +24,37 @@ pub struct ConfFile {
|
||||
/// correct path for WASM, JS, and Websockets, as well as other configuration tasks.
|
||||
/// It shares keys with cargo-leptos, to allow for easy interoperability
|
||||
#[derive(TypedBuilder, Debug, Clone, serde::Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct LeptosOptions {
|
||||
/// The name of the WASM and JS files generated by wasm-bindgen. Defaults to the crate name with underscores instead of dashes
|
||||
#[builder(setter(into))]
|
||||
pub output_name: String,
|
||||
/// The path of the all the files generated by cargo-leptos. This defaults to '.' for convenience when integrating with other
|
||||
/// tools.
|
||||
#[builder(setter(into), default=".".to_string())]
|
||||
#[builder(setter(into), default=default_site_root())]
|
||||
#[serde(default = "default_site_root")]
|
||||
pub site_root: String,
|
||||
/// The path of the WASM and JS files generated by wasm-bindgen from the root of your app
|
||||
/// By default, wasm-bindgen puts them in `pkg`.
|
||||
#[builder(setter(into), default="pkg".to_string())]
|
||||
#[builder(setter(into), default=default_site_pkg_dir())]
|
||||
#[serde(default = "default_site_pkg_dir")]
|
||||
pub site_pkg_dir: String,
|
||||
/// Used to configure the running environment of Leptos. Can be used to load dev constants and keys v prod, or change
|
||||
/// things based on the deployment environment
|
||||
/// I recommend passing in the result of `env::var("LEPTOS_ENV")`
|
||||
#[builder(setter(into), default=Env::DEV)]
|
||||
#[builder(setter(into), default=default_env())]
|
||||
#[serde(default = "default_env")]
|
||||
pub env: Env,
|
||||
/// Provides a way to control the address leptos is served from.
|
||||
/// Using an env variable here would allow you to run the same code in dev and prod
|
||||
/// Defaults to `127.0.0.1:3000`
|
||||
#[builder(setter(into), default=SocketAddr::from(([127,0,0,1], 3000)))]
|
||||
#[builder(setter(into), default=default_site_addr())]
|
||||
#[serde(default = "default_site_addr")]
|
||||
pub site_addr: SocketAddr,
|
||||
/// The port the Websocket watcher listens on. Should match the `reload_port` in cargo-leptos(if using).
|
||||
/// Defaults to `3001`
|
||||
#[builder(default = 3001)]
|
||||
#[builder(default = default_reload_port())]
|
||||
#[serde(default = "default_reload_port")]
|
||||
pub reload_port: u32,
|
||||
}
|
||||
|
||||
@@ -81,6 +88,26 @@ impl LeptosOptions {
|
||||
}
|
||||
}
|
||||
|
||||
fn default_site_root() -> String {
|
||||
".".to_string()
|
||||
}
|
||||
|
||||
fn default_site_pkg_dir() -> String {
|
||||
"pkg".to_string()
|
||||
}
|
||||
|
||||
fn default_env() -> Env {
|
||||
Env::DEV
|
||||
}
|
||||
|
||||
fn default_site_addr() -> SocketAddr {
|
||||
SocketAddr::from(([127, 0, 0, 1], 3000))
|
||||
}
|
||||
|
||||
fn default_reload_port() -> u32 {
|
||||
3001
|
||||
}
|
||||
|
||||
fn env_w_default(
|
||||
key: &str,
|
||||
default: &str,
|
||||
@@ -95,7 +122,7 @@ fn env_w_default(
|
||||
/// An enum that can be used to define the environment Leptos is running in.
|
||||
/// Setting this to the `PROD` variant will not include the WebSocket code for `cargo-leptos` watch mode.
|
||||
/// Defaults to `DEV`.
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
|
||||
pub enum Env {
|
||||
PROD,
|
||||
DEV,
|
||||
@@ -175,9 +202,7 @@ pub fn get_config_from_str(text: &str) -> Result<ConfFile, LeptosConfigError> {
|
||||
// so that serde error messages have right line number
|
||||
let newlines = text[..start].matches('\n').count();
|
||||
let input = "\n".repeat(newlines) + &text[start..];
|
||||
let toml = input
|
||||
.replace(metadata_name, "[leptos_options]")
|
||||
.replace('-', "_");
|
||||
let toml = input.replace(metadata_name, "[leptos-options]");
|
||||
let settings = Config::builder()
|
||||
// Read the "default" configuration file
|
||||
.add_source(File::from_str(&toml, FileFormat::Toml))
|
||||
|
||||
@@ -46,7 +46,7 @@ async fn get_configuration_from_file_ok() {
|
||||
.unwrap()
|
||||
.leptos_options;
|
||||
|
||||
assert_eq!(config.output_name, "app_test");
|
||||
assert_eq!(config.output_name, "app-test");
|
||||
assert_eq!(config.site_root, "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir, "my_pkg");
|
||||
assert_eq!(
|
||||
@@ -93,7 +93,7 @@ async fn get_config_from_file_ok() {
|
||||
.unwrap()
|
||||
.leptos_options;
|
||||
|
||||
assert_eq!(config.output_name, "app_test");
|
||||
assert_eq!(config.output_name, "app-test");
|
||||
assert_eq!(config.site_root, "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir, "my_pkg");
|
||||
assert_eq!(
|
||||
@@ -128,7 +128,7 @@ fn get_config_from_str_content() {
|
||||
let config = get_config_from_str(CARGO_TOML_CONTENT_OK)
|
||||
.unwrap()
|
||||
.leptos_options;
|
||||
assert_eq!(config.output_name, "app_test");
|
||||
assert_eq!(config.output_name, "app-test");
|
||||
assert_eq!(config.site_root, "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir, "my_pkg");
|
||||
assert_eq!(
|
||||
@@ -141,14 +141,14 @@ fn get_config_from_str_content() {
|
||||
#[tokio::test]
|
||||
async fn get_config_from_env() {
|
||||
// Test config values from environment variables
|
||||
std::env::set_var("LEPTOS_OUTPUT_NAME", "app_test");
|
||||
std::env::set_var("LEPTOS_OUTPUT_NAME", "app-test");
|
||||
std::env::set_var("LEPTOS_SITE_ROOT", "my_target/site");
|
||||
std::env::set_var("LEPTOS_SITE_PKG_DIR", "my_pkg");
|
||||
std::env::set_var("LEPTOS_SITE_ADDR", "0.0.0.0:80");
|
||||
std::env::set_var("LEPTOS_RELOAD_PORT", "8080");
|
||||
|
||||
let config = get_configuration(None).await.unwrap().leptos_options;
|
||||
assert_eq!(config.output_name, "app_test");
|
||||
assert_eq!(config.output_name, "app-test");
|
||||
|
||||
assert_eq!(config.site_root, "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir, "my_pkg");
|
||||
@@ -176,8 +176,8 @@ async fn get_config_from_env() {
|
||||
|
||||
#[test]
|
||||
fn leptos_options_builder_default() {
|
||||
let conf = LeptosOptions::builder().output_name("app_test").build();
|
||||
assert_eq!(conf.output_name, "app_test");
|
||||
let conf = LeptosOptions::builder().output_name("app-test").build();
|
||||
assert_eq!(conf.output_name, "app-test");
|
||||
assert!(matches!(conf.env, Env::DEV));
|
||||
assert_eq!(conf.site_pkg_dir, "pkg");
|
||||
assert_eq!(conf.site_root, ".");
|
||||
|
||||
@@ -9,12 +9,14 @@ description = "DOM operations for the Leptos web framework."
|
||||
|
||||
[dependencies]
|
||||
async-recursion = "1"
|
||||
base64 = { version = "0.21", optional = true }
|
||||
cfg-if = "1"
|
||||
drain_filter_polyfill = "0.1"
|
||||
educe = "0.4"
|
||||
futures = "0.3"
|
||||
getrandom = { version = "0.2", optional = true }
|
||||
html-escape = "0.2"
|
||||
indexmap = "1.9"
|
||||
indexmap = "2"
|
||||
itertools = "0.10"
|
||||
js-sys = "0.3"
|
||||
leptos_reactive = { workspace = true }
|
||||
@@ -22,6 +24,7 @@ server_fn = { workspace = true }
|
||||
once_cell = "1"
|
||||
pad-adapter = "0.1"
|
||||
paste = "1"
|
||||
rand = { version = "0.8", optional = true }
|
||||
rustc-hash = "1.1.0"
|
||||
serde_json = "1"
|
||||
smallvec = "1"
|
||||
@@ -29,6 +32,9 @@ tracing = "0.1"
|
||||
wasm-bindgen = { version = "0.2", features = ["enable-interning"] }
|
||||
wasm-bindgen-futures = "0.4.31"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
getrandom = { version = "0.2", features = ["js"] }
|
||||
|
||||
[dev-dependencies]
|
||||
leptos = { path = "../leptos" }
|
||||
|
||||
@@ -49,6 +55,7 @@ features = [
|
||||
"Range",
|
||||
"Text",
|
||||
"HtmlCollection",
|
||||
"ShadowRoot",
|
||||
"TreeWalker",
|
||||
|
||||
# Events we cast to in leptos_macro -- added here so we don't force users to import them
|
||||
@@ -160,6 +167,7 @@ default = []
|
||||
web = ["leptos_reactive/csr"]
|
||||
ssr = ["leptos_reactive/ssr"]
|
||||
nightly = ["leptos_reactive/nightly"]
|
||||
nonce = ["dep:base64", "dep:getrandom", "dep:rand"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["nightly"]
|
||||
|
||||
1
leptos_dom/Makefile.toml
Normal file
1
leptos_dom/Makefile.toml
Normal file
@@ -0,0 +1 @@
|
||||
extend = { path = "../cargo-make/main.toml" }
|
||||
@@ -158,19 +158,13 @@ pub(crate) fn add_delegated_event_listener(
|
||||
}
|
||||
|
||||
// navigate up tree
|
||||
let host =
|
||||
js_sys::Reflect::get(&node, &JsValue::from_str("host"))
|
||||
.unwrap_throw();
|
||||
if host.is_truthy()
|
||||
&& host != node
|
||||
&& host.dyn_ref::<web_sys::Node>().is_some()
|
||||
{
|
||||
node = host;
|
||||
} else if let Some(parent) =
|
||||
node.unchecked_into::<web_sys::Node>().parent_node()
|
||||
if let Some(parent) =
|
||||
node.unchecked_ref::<web_sys::Node>().parent_node()
|
||||
{
|
||||
node = parent.into()
|
||||
} else {
|
||||
} else if let Some(root) = node.dyn_ref::<web_sys::ShadowRoot>() {
|
||||
node = root.host().unchecked_into();
|
||||
} else {
|
||||
node = JsValue::null()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,11 +204,9 @@ impl Custom {
|
||||
assert_eq!(
|
||||
el.node_name().to_ascii_uppercase(),
|
||||
name.to_ascii_uppercase(),
|
||||
"SSR and CSR elements have the same `TopoId` but \
|
||||
different node kinds. This is either a discrepancy \
|
||||
between SSR and CSR rendering
|
||||
logic, which is considered a bug, or it can also be a \
|
||||
leptos hydration issue."
|
||||
"SSR and CSR elements have the same hydration key but \
|
||||
different node kinds. Check out the docs for information \
|
||||
about this kind of hydration bug: https://leptos-rs.github.io/leptos/ssr/24_hydration_bugs.html"
|
||||
);
|
||||
|
||||
el.remove_attribute("id").unwrap();
|
||||
@@ -221,11 +219,9 @@ impl Custom {
|
||||
assert_eq!(
|
||||
el.node_name().to_ascii_uppercase(),
|
||||
name.to_ascii_uppercase(),
|
||||
"SSR and CSR elements have the same `TopoId` but \
|
||||
different node kinds. This is either a discrepancy \
|
||||
between SSR and CSR rendering
|
||||
logic, which is considered a bug, or it can also be a \
|
||||
leptos hydration issue."
|
||||
"SSR and CSR elements have the same hydration key but \
|
||||
different node kinds. Check out the docs for information \
|
||||
about this kind of hydration bug: https://leptos-rs.github.io/leptos/ssr/24_hydration_bugs.html"
|
||||
);
|
||||
|
||||
el.remove_attribute("leptos-hk").unwrap();
|
||||
@@ -449,7 +445,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
element: AnyElement {
|
||||
name: element.name(),
|
||||
is_void: element.is_void(),
|
||||
id: *element.hydration_id()
|
||||
id: element.hydration_id().clone()
|
||||
},
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker
|
||||
@@ -1074,7 +1070,7 @@ impl<El: ElementDescriptor> IntoView for HtmlElement<El> {
|
||||
..
|
||||
} = self;
|
||||
|
||||
let id = *element.hydration_id();
|
||||
let id = element.hydration_id().clone();
|
||||
|
||||
let mut element = Element::new(element);
|
||||
let children = children;
|
||||
@@ -1118,7 +1114,7 @@ pub fn custom<El: ElementDescriptor>(cx: Scope, el: El) -> HtmlElement<Custom> {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
element: el.as_ref().clone(),
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
id: *el.hydration_id(),
|
||||
id: el.hydration_id().clone(),
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -1270,11 +1266,9 @@ fn create_leptos_element(
|
||||
assert_eq!(
|
||||
&el.node_name().to_ascii_uppercase(),
|
||||
tag,
|
||||
"SSR and CSR elements have the same `TopoId` but different \
|
||||
node kinds. This is either a discrepancy between SSR and CSR \
|
||||
rendering
|
||||
logic, which is considered a bug, or it can also be a leptos \
|
||||
hydration issue."
|
||||
"SSR and CSR elements have the same hydration key but \
|
||||
different node kinds. Check out the docs for information \
|
||||
about this kind of hydration bug: https://leptos-rs.github.io/leptos/ssr/24_hydration_bugs.html"
|
||||
);
|
||||
|
||||
el.remove_attribute("id").unwrap();
|
||||
@@ -1287,11 +1281,9 @@ fn create_leptos_element(
|
||||
assert_eq!(
|
||||
el.node_name().to_ascii_uppercase(),
|
||||
tag,
|
||||
"SSR and CSR elements have the same `TopoId` but different \
|
||||
node kinds. This is either a discrepancy between SSR and CSR \
|
||||
rendering
|
||||
logic, which is considered a bug, or it can also be a leptos \
|
||||
hydration issue."
|
||||
"SSR and CSR elements have the same hydration key but \
|
||||
different node kinds. Check out the docs for information \
|
||||
about this kind of hydration bug: https://leptos-rs.github.io/leptos/ssr/24_hydration_bugs.html"
|
||||
);
|
||||
|
||||
el.remove_attribute("leptos-hk").unwrap();
|
||||
|
||||
@@ -50,13 +50,13 @@ cfg_if! {
|
||||
|
||||
static IS_HYDRATING: RefCell<LazyCell<bool>> = RefCell::new(LazyCell::new(|| {
|
||||
#[cfg(debug_assertions)]
|
||||
return crate::document().get_element_by_id("_0-1").is_some()
|
||||
|| crate::document().get_element_by_id("_0-1o").is_some()
|
||||
|| HYDRATION_COMMENTS.with(|comments| comments.get("_0-1o").is_some());
|
||||
return crate::document().get_element_by_id("_-1").is_some()
|
||||
|| crate::document().get_element_by_id("_-1o").is_some()
|
||||
|| HYDRATION_COMMENTS.with(|comments| comments.get("_-1o").is_some());
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
return crate::document().get_element_by_id("_0-1").is_some()
|
||||
|| HYDRATION_COMMENTS.with(|comments| comments.get("_0-1").is_some());
|
||||
return crate::document().get_element_by_id("_-1").is_some()
|
||||
|| HYDRATION_COMMENTS.with(|comments| comments.get("_-1").is_some());
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -67,12 +67,12 @@ cfg_if! {
|
||||
}
|
||||
|
||||
/// A stable identifier within the server-rendering or hydration process.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, Default)]
|
||||
pub struct HydrationKey {
|
||||
/// ID of the current key.
|
||||
pub id: usize,
|
||||
/// ID of the current fragment.
|
||||
pub fragment: usize,
|
||||
pub fragment: String,
|
||||
}
|
||||
|
||||
impl Display for HydrationKey {
|
||||
@@ -89,7 +89,7 @@ pub struct HydrationCtx;
|
||||
impl HydrationCtx {
|
||||
/// Get the next `id` without incrementing it.
|
||||
pub fn peek() -> HydrationKey {
|
||||
ID.with(|id| *id.borrow())
|
||||
ID.with(|id| id.borrow().clone())
|
||||
}
|
||||
|
||||
/// Increments the current hydration `id` and returns it
|
||||
@@ -97,17 +97,17 @@ impl HydrationCtx {
|
||||
ID.with(|id| {
|
||||
let mut id = id.borrow_mut();
|
||||
id.id = id.id.wrapping_add(1);
|
||||
*id
|
||||
id.clone()
|
||||
})
|
||||
}
|
||||
|
||||
/// Resets the hydration `id` for the next component, and returns it
|
||||
pub fn next_component() -> HydrationKey {
|
||||
pub fn next_component(tag: &'static str) -> HydrationKey {
|
||||
ID.with(|id| {
|
||||
let mut id = id.borrow_mut();
|
||||
id.fragment = id.fragment.wrapping_add(1);
|
||||
id.fragment = format!("{}-{}{}", id.fragment, id.id, tag);
|
||||
id.id = 0;
|
||||
*id
|
||||
id.clone()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ mod logging;
|
||||
mod macro_helpers;
|
||||
pub mod math;
|
||||
mod node_ref;
|
||||
/// Utilities for exporting nonces to be used for a Content Security Policy.
|
||||
pub mod nonce;
|
||||
pub mod ssr;
|
||||
pub mod ssr_in_order;
|
||||
pub mod svg;
|
||||
@@ -376,7 +378,7 @@ impl Element {
|
||||
is_void: el.is_void(),
|
||||
attrs: Default::default(),
|
||||
children: Default::default(),
|
||||
id: *el.hydration_id(),
|
||||
id: el.hydration_id().clone(),
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker: None
|
||||
}
|
||||
@@ -732,7 +734,7 @@ impl View {
|
||||
c.children.iter().cloned().for_each(|c| {
|
||||
let event_handler = event_handler.clone();
|
||||
|
||||
c.on(event.clone(), Box::new(move |e| event_handler.borrow_mut()(e)));
|
||||
_ = c.on(event.clone(), Box::new(move |e| event_handler.borrow_mut()(e)));
|
||||
});
|
||||
}
|
||||
Self::CoreComponent(c) => match c {
|
||||
|
||||
@@ -67,11 +67,9 @@ macro_rules! generate_math_tags {
|
||||
assert_eq!(
|
||||
el.node_name().to_ascii_uppercase(),
|
||||
stringify!([<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]),
|
||||
"SSR and CSR elements have the same `TopoId` \
|
||||
but different node kinds. This is either a \
|
||||
discrepancy between SSR and CSR rendering
|
||||
logic, which is considered a bug, or it \
|
||||
can also be a leptos hydration issue."
|
||||
"SSR and CSR elements have the same hydration key but \
|
||||
different node kinds. Check out the docs for information \
|
||||
about this kind of hydration bug: https://leptos-rs.github.io/leptos/ssr/24_hydration_bugs.html"
|
||||
);
|
||||
|
||||
el.remove_attribute("id").unwrap();
|
||||
@@ -84,11 +82,9 @@ macro_rules! generate_math_tags {
|
||||
assert_eq!(
|
||||
el.node_name().to_ascii_uppercase(),
|
||||
stringify!([<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]),
|
||||
"SSR and CSR elements have the same `TopoId` \
|
||||
but different node kinds. This is either a \
|
||||
discrepancy between SSR and CSR rendering
|
||||
logic, which is considered a bug, or it \
|
||||
can also be a leptos hydration issue."
|
||||
"SSR and CSR elements have the same hydration key but \
|
||||
different node kinds. Check out the docs for information \
|
||||
about this kind of hydration bug: https://leptos-rs.github.io/leptos/ssr/24_hydration_bugs.html"
|
||||
);
|
||||
|
||||
el.remove_attribute("leptos-hk").unwrap();
|
||||
|
||||
158
leptos_dom/src/nonce.rs
Normal file
158
leptos_dom/src/nonce.rs
Normal file
@@ -0,0 +1,158 @@
|
||||
use crate::{Attribute, IntoAttribute};
|
||||
use leptos_reactive::{use_context, Scope};
|
||||
use std::{fmt::Display, ops::Deref};
|
||||
|
||||
/// A nonce a cryptographic nonce ("number used once") which can be
|
||||
/// used by Content Security Policy to determine whether or not a given
|
||||
/// resource will be allowed to load.
|
||||
///
|
||||
/// When the `nonce` feature is enabled on one of the server integrations,
|
||||
/// a nonce is generated during server rendering and added to all inline
|
||||
/// scripts used for HTML streaming and resource loading.
|
||||
///
|
||||
/// The nonce being used during the current server response can be
|
||||
/// accessed using [`use_nonce`](use_nonce).
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[component]
|
||||
/// pub fn App(cx: Scope) -> impl IntoView {
|
||||
/// provide_meta_context(cx);
|
||||
///
|
||||
/// view! { cx,
|
||||
/// // use `leptos_meta` to insert a <meta> tag with the CSP
|
||||
/// <Meta
|
||||
/// http_equiv="Content-Security-Policy"
|
||||
/// content=move || {
|
||||
/// // this will insert the CSP with nonce on the server, be empty on client
|
||||
/// use_nonce(cx)
|
||||
/// .map(|nonce| {
|
||||
/// format!(
|
||||
/// "default-src 'self'; script-src 'strict-dynamic' 'nonce-{nonce}' \
|
||||
/// 'wasm-unsafe-eval'; style-src 'nonce-{nonce}';"
|
||||
/// )
|
||||
/// })
|
||||
/// .unwrap_or_default()
|
||||
/// }
|
||||
/// />
|
||||
/// // manually insert nonce during SSR on inline script
|
||||
/// <script nonce=use_nonce(cx)>"console.log('Hello, world!');"</script>
|
||||
/// // leptos_meta <Style/> and <Script/> automatically insert the nonce
|
||||
/// <Style>"body { color: blue; }"</Style>
|
||||
/// <p>"Test"</p>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct Nonce(pub(crate) String);
|
||||
|
||||
impl Deref for Nonce {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Nonce {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoAttribute for Nonce {
|
||||
fn into_attribute(self, _cx: Scope) -> Attribute {
|
||||
Attribute::String(self.0.into())
|
||||
}
|
||||
|
||||
fn into_attribute_boxed(self: Box<Self>, _cx: Scope) -> Attribute {
|
||||
Attribute::String(self.0.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoAttribute for Option<Nonce> {
|
||||
fn into_attribute(self, cx: Scope) -> Attribute {
|
||||
Attribute::Option(cx, self.map(|n| n.0.into()))
|
||||
}
|
||||
|
||||
fn into_attribute_boxed(self: Box<Self>, cx: Scope) -> Attribute {
|
||||
Attribute::Option(cx, self.map(|n| n.0.into()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Accesses the nonce that has been generated during the current
|
||||
/// server response. This can be added to inline `<script>` and
|
||||
/// `<style>` tags for compatibility with a Content Security Policy.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[component]
|
||||
/// pub fn App(cx: Scope) -> impl IntoView {
|
||||
/// provide_meta_context(cx);
|
||||
///
|
||||
/// view! { cx,
|
||||
/// // use `leptos_meta` to insert a <meta> tag with the CSP
|
||||
/// <Meta
|
||||
/// http_equiv="Content-Security-Policy"
|
||||
/// content=move || {
|
||||
/// // this will insert the CSP with nonce on the server, be empty on client
|
||||
/// use_nonce(cx)
|
||||
/// .map(|nonce| {
|
||||
/// format!(
|
||||
/// "default-src 'self'; script-src 'strict-dynamic' 'nonce-{nonce}' \
|
||||
/// 'wasm-unsafe-eval'; style-src 'nonce-{nonce}';"
|
||||
/// )
|
||||
/// })
|
||||
/// .unwrap_or_default()
|
||||
/// }
|
||||
/// />
|
||||
/// // manually insert nonce during SSR on inline script
|
||||
/// <script nonce=use_nonce(cx)>"console.log('Hello, world!');"</script>
|
||||
/// // leptos_meta <Style/> and <Script/> automatically insert the nonce
|
||||
/// <Style>"body { color: blue; }"</Style>
|
||||
/// <p>"Test"</p>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn use_nonce(cx: Scope) -> Option<Nonce> {
|
||||
use_context::<Nonce>(cx)
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "ssr", feature = "nonce"))]
|
||||
pub use generate::*;
|
||||
|
||||
#[cfg(all(feature = "ssr", feature = "nonce"))]
|
||||
mod generate {
|
||||
use super::Nonce;
|
||||
use base64::{
|
||||
alphabet,
|
||||
engine::{self, general_purpose},
|
||||
Engine,
|
||||
};
|
||||
use leptos_reactive::{provide_context, Scope};
|
||||
use rand::{thread_rng, RngCore};
|
||||
|
||||
const NONCE_ENGINE: engine::GeneralPurpose = engine::GeneralPurpose::new(
|
||||
&alphabet::URL_SAFE,
|
||||
general_purpose::NO_PAD,
|
||||
);
|
||||
|
||||
impl Nonce {
|
||||
/// Generates a new nonce from 16 bytes (128 bits) of random data.
|
||||
pub fn new() -> Self {
|
||||
let mut thread_rng = thread_rng();
|
||||
let mut bytes = [0; 16];
|
||||
thread_rng.fill_bytes(&mut bytes);
|
||||
Nonce(NONCE_ENGINE.encode(bytes))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Nonce {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a nonce and provides it during server rendering.
|
||||
pub fn provide_nonce(cx: Scope) {
|
||||
provide_context(cx, Nonce::new())
|
||||
}
|
||||
}
|
||||
@@ -212,6 +212,9 @@ pub fn render_to_stream_with_prefix_undisposed_with_context_and_block_replacemen
|
||||
}
|
||||
});
|
||||
let cx = Scope { runtime, id: scope };
|
||||
let nonce_str = crate::nonce::use_nonce(cx)
|
||||
.map(|nonce| format!(" nonce=\"{nonce}\""))
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut blocking_fragments = FuturesUnordered::new();
|
||||
let fragments = FuturesUnordered::new();
|
||||
@@ -230,91 +233,110 @@ pub fn render_to_stream_with_prefix_undisposed_with_context_and_block_replacemen
|
||||
|
||||
let stream = futures::stream::once(
|
||||
// HTML for the view function and script to store resources
|
||||
async move {
|
||||
let resolvers = format!(
|
||||
"<script>__LEPTOS_PENDING_RESOURCES = \
|
||||
{pending_resources};__LEPTOS_RESOLVED_RESOURCES = new \
|
||||
Map();__LEPTOS_RESOURCE_RESOLVERS = new Map();</script>"
|
||||
);
|
||||
{
|
||||
let nonce_str = nonce_str.clone();
|
||||
async move {
|
||||
let resolvers = format!(
|
||||
"<script{nonce_str}>__LEPTOS_PENDING_RESOURCES = \
|
||||
{pending_resources};__LEPTOS_RESOLVED_RESOURCES = new \
|
||||
Map();__LEPTOS_RESOURCE_RESOLVERS = new Map();</script>"
|
||||
);
|
||||
|
||||
if replace_blocks {
|
||||
let mut blocks = Vec::with_capacity(blocking_fragments.len());
|
||||
while let Some((blocked_id, blocked_fragment)) =
|
||||
blocking_fragments.next().await
|
||||
{
|
||||
blocks.push((blocked_id, blocked_fragment));
|
||||
if replace_blocks {
|
||||
let mut blocks =
|
||||
Vec::with_capacity(blocking_fragments.len());
|
||||
while let Some((blocked_id, blocked_fragment)) =
|
||||
blocking_fragments.next().await
|
||||
{
|
||||
blocks.push((blocked_id, blocked_fragment));
|
||||
}
|
||||
|
||||
let prefix = prefix(cx);
|
||||
|
||||
let mut shell = shell;
|
||||
|
||||
for (blocked_id, blocked_fragment) in blocks {
|
||||
let open = format!("<!--suspense-open-{blocked_id}-->");
|
||||
let close =
|
||||
format!("<!--suspense-close-{blocked_id}-->");
|
||||
let (first, rest) =
|
||||
shell.split_once(&open).unwrap_or_default();
|
||||
let (_fallback, rest) =
|
||||
rest.split_once(&close).unwrap_or_default();
|
||||
|
||||
shell =
|
||||
format!("{first}{blocked_fragment}{rest}").into();
|
||||
}
|
||||
|
||||
format!("{prefix}{shell}{resolvers}")
|
||||
} else {
|
||||
let mut blocking = String::new();
|
||||
let mut blocking_fragments = fragments_to_chunks(
|
||||
nonce_str.clone(),
|
||||
blocking_fragments,
|
||||
);
|
||||
|
||||
while let Some(fragment) = blocking_fragments.next().await {
|
||||
blocking.push_str(&fragment);
|
||||
}
|
||||
let prefix = prefix(cx);
|
||||
format!("{prefix}{shell}{resolvers}{blocking}")
|
||||
}
|
||||
|
||||
let prefix = prefix(cx);
|
||||
|
||||
let mut shell = shell;
|
||||
|
||||
for (blocked_id, blocked_fragment) in blocks {
|
||||
let open = format!("<!--suspense-open-{blocked_id}-->");
|
||||
let close = format!("<!--suspense-close-{blocked_id}-->");
|
||||
let (first, rest) =
|
||||
shell.split_once(&open).unwrap_or_default();
|
||||
let (_fallback, rest) =
|
||||
rest.split_once(&close).unwrap_or_default();
|
||||
|
||||
shell = format!("{first}{blocked_fragment}{rest}").into();
|
||||
}
|
||||
|
||||
format!("{prefix}{shell}{resolvers}")
|
||||
} else {
|
||||
let mut blocking = String::new();
|
||||
let mut blocking_fragments =
|
||||
fragments_to_chunks(blocking_fragments);
|
||||
|
||||
while let Some(fragment) = blocking_fragments.next().await {
|
||||
blocking.push_str(&fragment);
|
||||
}
|
||||
let prefix = prefix(cx);
|
||||
format!("{prefix}{shell}{resolvers}{blocking}")
|
||||
}
|
||||
},
|
||||
)
|
||||
.chain(ooo_body_stream_recurse(cx, fragments, serializers));
|
||||
.chain(ooo_body_stream_recurse(
|
||||
cx,
|
||||
nonce_str,
|
||||
fragments,
|
||||
serializers,
|
||||
));
|
||||
|
||||
(stream, runtime, scope)
|
||||
}
|
||||
|
||||
fn ooo_body_stream_recurse(
|
||||
cx: Scope,
|
||||
nonce_str: String,
|
||||
fragments: FuturesUnordered<PinnedFuture<(String, String)>>,
|
||||
serializers: FuturesUnordered<PinnedFuture<(ResourceId, String)>>,
|
||||
) -> Pin<Box<dyn Stream<Item = String>>> {
|
||||
// resources and fragments
|
||||
// stream HTML for each <Suspense/> as it resolves
|
||||
let fragments = fragments_to_chunks(fragments);
|
||||
let fragments = fragments_to_chunks(nonce_str.clone(), fragments);
|
||||
// stream data for each Resource as it resolves
|
||||
let resources = render_serializers(serializers);
|
||||
let resources = render_serializers(nonce_str.clone(), serializers);
|
||||
|
||||
Box::pin(
|
||||
// TODO these should be combined again in a way that chains them appropriately
|
||||
// such that individual resources can resolve before all fragments are done
|
||||
fragments.chain(resources).chain(
|
||||
futures::stream::once(async move {
|
||||
let pending = cx.pending_fragments();
|
||||
if !pending.is_empty() {
|
||||
let fragments = FuturesUnordered::new();
|
||||
let serializers = cx.serialization_resolvers();
|
||||
for (fragment_id, data) in pending {
|
||||
fragments.push(Box::pin(async move {
|
||||
(fragment_id.clone(), data.out_of_order.await)
|
||||
})
|
||||
as Pin<Box<dyn Future<Output = (String, String)>>>);
|
||||
futures::stream::once({
|
||||
async move {
|
||||
let pending = cx.pending_fragments();
|
||||
if !pending.is_empty() {
|
||||
let fragments = FuturesUnordered::new();
|
||||
let serializers = cx.serialization_resolvers();
|
||||
for (fragment_id, data) in pending {
|
||||
fragments.push(Box::pin(async move {
|
||||
(fragment_id.clone(), data.out_of_order.await)
|
||||
})
|
||||
as Pin<
|
||||
Box<dyn Future<Output = (String, String)>>,
|
||||
>);
|
||||
}
|
||||
Box::pin(ooo_body_stream_recurse(
|
||||
cx,
|
||||
nonce_str.clone(),
|
||||
fragments,
|
||||
serializers,
|
||||
))
|
||||
as Pin<Box<dyn Stream<Item = String>>>
|
||||
} else {
|
||||
Box::pin(futures::stream::once(async move {
|
||||
Default::default()
|
||||
}))
|
||||
}
|
||||
Box::pin(ooo_body_stream_recurse(
|
||||
cx,
|
||||
fragments,
|
||||
serializers,
|
||||
))
|
||||
as Pin<Box<dyn Stream<Item = String>>>
|
||||
} else {
|
||||
Box::pin(futures::stream::once(async move {
|
||||
Default::default()
|
||||
}))
|
||||
}
|
||||
})
|
||||
.flatten(),
|
||||
@@ -327,13 +349,14 @@ fn ooo_body_stream_recurse(
|
||||
instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
fn fragments_to_chunks(
|
||||
nonce_str: String,
|
||||
fragments: impl Stream<Item = (String, String)>,
|
||||
) -> impl Stream<Item = String> {
|
||||
fragments.map(|(fragment_id, html)| {
|
||||
fragments.map(move |(fragment_id, html)| {
|
||||
format!(
|
||||
r#"
|
||||
<template id="{fragment_id}f">{html}</template>
|
||||
<script>
|
||||
<script{nonce_str}>
|
||||
var id = "{fragment_id}";
|
||||
var open = undefined;
|
||||
var close = undefined;
|
||||
@@ -430,7 +453,7 @@ impl View {
|
||||
View::CoreComponent(node) => {
|
||||
let (id, name, wrap, content) = match node {
|
||||
CoreComponent::Unit(u) => (
|
||||
u.id,
|
||||
u.id.clone(),
|
||||
"",
|
||||
false,
|
||||
Box::new(move || {
|
||||
@@ -679,13 +702,14 @@ pub(crate) fn to_kebab_case(name: &str) -> String {
|
||||
instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
pub(crate) fn render_serializers(
|
||||
nonce_str: String,
|
||||
serializers: FuturesUnordered<PinnedFuture<(ResourceId, String)>>,
|
||||
) -> impl Stream<Item = String> {
|
||||
serializers.map(|(id, json)| {
|
||||
serializers.map(move |(id, json)| {
|
||||
let id = serde_json::to_string(&id).unwrap();
|
||||
let json = json.replace('<', "\\u003c");
|
||||
format!(
|
||||
r#"<script>
|
||||
r#"<script{nonce_str}>
|
||||
var val = {json:?};
|
||||
if(__LEPTOS_RESOURCE_RESOLVERS.get({id})) {{
|
||||
__LEPTOS_RESOURCE_RESOLVERS.get({id})(val)
|
||||
|
||||
@@ -124,24 +124,33 @@ pub fn render_to_stream_in_order_with_prefix_undisposed_with_context(
|
||||
handle_chunks(cx, tx, remaining_chunks).await;
|
||||
});
|
||||
|
||||
let stream = futures::stream::once(async move {
|
||||
let prefix = prefix_rx.await.expect("to receive prefix");
|
||||
format!(
|
||||
r#"
|
||||
let nonce = crate::nonce::use_nonce(cx);
|
||||
let nonce_str = nonce
|
||||
.as_ref()
|
||||
.map(|nonce| format!(" nonce=\"{nonce}\""))
|
||||
.unwrap_or_default();
|
||||
|
||||
let stream = futures::stream::once({
|
||||
let nonce_str = nonce_str.clone();
|
||||
async move {
|
||||
let prefix = prefix_rx.await.expect("to receive prefix");
|
||||
format!(
|
||||
r#"
|
||||
{prefix}
|
||||
<script>
|
||||
<script{nonce_str}>
|
||||
__LEPTOS_PENDING_RESOURCES = {pending_resources};
|
||||
__LEPTOS_RESOLVED_RESOURCES = new Map();
|
||||
__LEPTOS_RESOURCE_RESOLVERS = new Map();
|
||||
</script>
|
||||
"#
|
||||
)
|
||||
)
|
||||
}
|
||||
})
|
||||
.chain(rx)
|
||||
.chain(
|
||||
futures::stream::once(async move {
|
||||
let serializers = cx.serialization_resolvers();
|
||||
render_serializers(serializers)
|
||||
render_serializers(nonce_str, serializers)
|
||||
})
|
||||
.flatten(),
|
||||
);
|
||||
@@ -372,7 +381,7 @@ impl View {
|
||||
View::CoreComponent(node) => {
|
||||
let (id, name, wrap, content) = match node {
|
||||
CoreComponent::Unit(u) => (
|
||||
u.id,
|
||||
u.id.clone(),
|
||||
"",
|
||||
false,
|
||||
Box::new(move |chunks: &mut VecDeque<StreamChunk>| {
|
||||
|
||||
@@ -64,11 +64,9 @@ macro_rules! generate_svg_tags {
|
||||
assert_eq!(
|
||||
el.node_name().to_ascii_uppercase(),
|
||||
stringify!([<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]),
|
||||
"SSR and CSR elements have the same `TopoId` \
|
||||
but different node kinds. This is either a \
|
||||
discrepancy between SSR and CSR rendering
|
||||
logic, which is considered a bug, or it \
|
||||
can also be a leptos hydration issue."
|
||||
"SSR and CSR elements have the same hydration key but \
|
||||
different node kinds. Check out the docs for information \
|
||||
about this kind of hydration bug: https://leptos-rs.github.io/leptos/ssr/24_hydration_bugs.html"
|
||||
);
|
||||
|
||||
el.remove_attribute("id").unwrap();
|
||||
@@ -81,11 +79,9 @@ macro_rules! generate_svg_tags {
|
||||
assert_eq!(
|
||||
el.node_name().to_ascii_uppercase(),
|
||||
stringify!([<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]),
|
||||
"SSR and CSR elements have the same `TopoId` \
|
||||
but different node kinds. This is either a \
|
||||
discrepancy between SSR and CSR rendering
|
||||
logic, which is considered a bug, or it \
|
||||
can also be a leptos hydration issue."
|
||||
"SSR and CSR elements have the same hydration key but \
|
||||
different node kinds. Check out the docs for information \
|
||||
about this kind of hydration bug: https://leptos-rs.github.io/leptos/ssr/24_hydration_bugs.html"
|
||||
);
|
||||
|
||||
el.remove_attribute("leptos-hk").unwrap();
|
||||
|
||||
@@ -24,4 +24,4 @@ proc-macro2 = { version = "1", features = ["span-locations", "nightly"] }
|
||||
parking_lot = "0.12"
|
||||
walkdir = "2"
|
||||
camino = "1.1.3"
|
||||
indexmap = "1.9.2"
|
||||
indexmap = "2"
|
||||
|
||||
1
leptos_hot_reload/Makefile.toml
Normal file
1
leptos_hot_reload/Makefile.toml
Normal file
@@ -0,0 +1 @@
|
||||
extend = { path = "../cargo-make/main.toml" }
|
||||
23
leptos_macro/Makefile.toml
Normal file
23
leptos_macro/Makefile.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
extend = { path = "../cargo-make/main.toml" }
|
||||
|
||||
[tasks.test]
|
||||
clear = true
|
||||
dependencies = [
|
||||
"test-all",
|
||||
"test-leptos_macro-example",
|
||||
"doc-leptos_macro-example",
|
||||
]
|
||||
|
||||
[tasks.test-leptos_macro-example]
|
||||
description = "Tests the leptos_macro/example to check if macro handles doc comments correctly"
|
||||
command = "cargo"
|
||||
args = ["+nightly", "test", "--doc"]
|
||||
cwd = "example"
|
||||
install_crate = false
|
||||
|
||||
[tasks.doc-leptos_macro-example]
|
||||
description = "Docs the leptos_macro/example to check if macro handles doc comments correctly"
|
||||
command = "cargo"
|
||||
args = ["+nightly", "doc"]
|
||||
cwd = "example"
|
||||
install_crate = false
|
||||
@@ -1553,6 +1553,7 @@ pub(crate) fn component_to_tokens(
|
||||
global_class: Option<&TokenTree>,
|
||||
) -> TokenStream {
|
||||
let name = node.name();
|
||||
#[cfg(debug_assertions)]
|
||||
let component_name = ident_from_tag_name(node.name());
|
||||
let span = node.name().span();
|
||||
|
||||
@@ -1685,6 +1686,7 @@ pub(crate) fn component_to_tokens(
|
||||
}
|
||||
});
|
||||
|
||||
#[allow(unused_mut)] // used in debug
|
||||
let mut component = quote! {
|
||||
::leptos::component_view(
|
||||
&#name,
|
||||
@@ -2120,6 +2122,7 @@ impl IdeTagHelper {
|
||||
/// open_tag(open_tag.props().slots().children().build())
|
||||
/// }
|
||||
/// ```
|
||||
#[cfg(debug_assertions)]
|
||||
pub fn add_component_completion(
|
||||
component: &mut TokenStream,
|
||||
node: &NodeElement,
|
||||
|
||||
@@ -41,7 +41,7 @@ web-sys = { version = "0.3", optional = true, features = [
|
||||
"Window",
|
||||
] }
|
||||
cfg-if = "1"
|
||||
indexmap = "1"
|
||||
indexmap = "2"
|
||||
self_cell = "1.0.0"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
1
leptos_reactive/Makefile.toml
Normal file
1
leptos_reactive/Makefile.toml
Normal file
@@ -0,0 +1 @@
|
||||
extend = { path = "../cargo-make/main.toml" }
|
||||
@@ -863,6 +863,21 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
// on cleanup of this component, remove this read from parent `<Suspense/>`
|
||||
// it will be added back in when this is rendered again
|
||||
if let Some(s) = suspense_cx {
|
||||
crate::on_cleanup(cx, {
|
||||
let suspense_contexts = Rc::clone(&suspense_contexts);
|
||||
move || {
|
||||
if let Ok(ref mut contexts) =
|
||||
suspense_contexts.try_borrow_mut()
|
||||
{
|
||||
contexts.remove(&s);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let increment = move |_: Option<()>| {
|
||||
if let Some(s) = &suspense_cx {
|
||||
if let Ok(ref mut contexts) = suspense_contexts.try_borrow_mut()
|
||||
@@ -966,10 +981,8 @@ where
|
||||
|
||||
if version == last_version.get() {
|
||||
resolved.set(true);
|
||||
|
||||
set_value.update(|n| *n = Some(res));
|
||||
|
||||
set_loading.update(|n| *n = false);
|
||||
set_value.try_update(|n| *n = Some(res));
|
||||
set_loading.try_update(|n| *n = false);
|
||||
|
||||
for suspense_context in
|
||||
suspense_contexts.borrow().iter()
|
||||
|
||||
@@ -893,8 +893,12 @@ where
|
||||
)
|
||||
)]
|
||||
fn set_untracked(&self, new_value: T) {
|
||||
self.id
|
||||
.update_with_no_effect(self.runtime, |v| *v = new_value);
|
||||
self.id.update_with_no_effect(
|
||||
self.runtime,
|
||||
|v| *v = new_value,
|
||||
#[cfg(debug_assertions)]
|
||||
Some(self.defined_at),
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
@@ -913,8 +917,12 @@ where
|
||||
fn try_set_untracked(&self, new_value: T) -> Option<T> {
|
||||
let mut new_value = Some(new_value);
|
||||
|
||||
self.id
|
||||
.update(self.runtime, |t| *t = new_value.take().unwrap());
|
||||
self.id.update(
|
||||
self.runtime,
|
||||
|t| *t = new_value.take().unwrap(),
|
||||
#[cfg(debug_assertions)]
|
||||
None,
|
||||
);
|
||||
|
||||
new_value
|
||||
}
|
||||
@@ -936,7 +944,12 @@ impl<T> SignalUpdateUntracked<T> for WriteSignal<T> {
|
||||
)]
|
||||
#[inline(always)]
|
||||
fn update_untracked(&self, f: impl FnOnce(&mut T)) {
|
||||
self.id.update_with_no_effect(self.runtime, f);
|
||||
self.id.update_with_no_effect(
|
||||
self.runtime,
|
||||
f,
|
||||
#[cfg(debug_assertions)]
|
||||
Some(self.defined_at),
|
||||
);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
@@ -944,7 +957,12 @@ impl<T> SignalUpdateUntracked<T> for WriteSignal<T> {
|
||||
&self,
|
||||
f: impl FnOnce(&mut T) -> O,
|
||||
) -> Option<O> {
|
||||
self.id.update_with_no_effect(self.runtime, f)
|
||||
self.id.update_with_no_effect(
|
||||
self.runtime,
|
||||
f,
|
||||
#[cfg(debug_assertions)]
|
||||
None,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -980,7 +998,16 @@ impl<T> SignalUpdate<T> for WriteSignal<T> {
|
||||
)]
|
||||
#[inline(always)]
|
||||
fn update(&self, f: impl FnOnce(&mut T)) {
|
||||
if self.id.update(self.runtime, f).is_none() {
|
||||
if self
|
||||
.id
|
||||
.update(
|
||||
self.runtime,
|
||||
f,
|
||||
#[cfg(debug_assertions)]
|
||||
Some(self.defined_at),
|
||||
)
|
||||
.is_none()
|
||||
{
|
||||
warn_updating_dead_signal(
|
||||
#[cfg(any(debug_assertions, feature = "ssr"))]
|
||||
self.defined_at,
|
||||
@@ -1003,7 +1030,12 @@ impl<T> SignalUpdate<T> for WriteSignal<T> {
|
||||
)]
|
||||
#[inline(always)]
|
||||
fn try_update<O>(&self, f: impl FnOnce(&mut T) -> O) -> Option<O> {
|
||||
self.id.update(self.runtime, f)
|
||||
self.id.update(
|
||||
self.runtime,
|
||||
f,
|
||||
#[cfg(debug_assertions)]
|
||||
None,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1039,7 +1071,12 @@ impl<T> SignalSet<T> for WriteSignal<T> {
|
||||
)
|
||||
)]
|
||||
fn set(&self, new_value: T) {
|
||||
self.id.update(self.runtime, |n| *n = new_value);
|
||||
self.id.update(
|
||||
self.runtime,
|
||||
|n| *n = new_value,
|
||||
#[cfg(debug_assertions)]
|
||||
Some(self.defined_at),
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
@@ -1058,8 +1095,12 @@ impl<T> SignalSet<T> for WriteSignal<T> {
|
||||
fn try_set(&self, new_value: T) -> Option<T> {
|
||||
let mut new_value = Some(new_value);
|
||||
|
||||
self.id
|
||||
.update(self.runtime, |t| *t = new_value.take().unwrap());
|
||||
self.id.update(
|
||||
self.runtime,
|
||||
|t| *t = new_value.take().unwrap(),
|
||||
#[cfg(debug_assertions)]
|
||||
None,
|
||||
);
|
||||
|
||||
new_value
|
||||
}
|
||||
@@ -1340,8 +1381,12 @@ impl<T> SignalSetUntracked<T> for RwSignal<T> {
|
||||
)
|
||||
)]
|
||||
fn set_untracked(&self, new_value: T) {
|
||||
self.id
|
||||
.update_with_no_effect(self.runtime, |v| *v = new_value);
|
||||
self.id.update_with_no_effect(
|
||||
self.runtime,
|
||||
|v| *v = new_value,
|
||||
#[cfg(debug_assertions)]
|
||||
Some(self.defined_at),
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
@@ -1360,8 +1405,12 @@ impl<T> SignalSetUntracked<T> for RwSignal<T> {
|
||||
fn try_set_untracked(&self, new_value: T) -> Option<T> {
|
||||
let mut new_value = Some(new_value);
|
||||
|
||||
self.id
|
||||
.update(self.runtime, |t| *t = new_value.take().unwrap());
|
||||
self.id.update(
|
||||
self.runtime,
|
||||
|t| *t = new_value.take().unwrap(),
|
||||
#[cfg(debug_assertions)]
|
||||
None,
|
||||
);
|
||||
|
||||
new_value
|
||||
}
|
||||
@@ -1383,7 +1432,12 @@ impl<T> SignalUpdateUntracked<T> for RwSignal<T> {
|
||||
)]
|
||||
#[inline(always)]
|
||||
fn update_untracked(&self, f: impl FnOnce(&mut T)) {
|
||||
self.id.update_with_no_effect(self.runtime, f);
|
||||
self.id.update_with_no_effect(
|
||||
self.runtime,
|
||||
f,
|
||||
#[cfg(debug_assertions)]
|
||||
Some(self.defined_at),
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
@@ -1404,7 +1458,12 @@ impl<T> SignalUpdateUntracked<T> for RwSignal<T> {
|
||||
&self,
|
||||
f: impl FnOnce(&mut T) -> O,
|
||||
) -> Option<O> {
|
||||
self.id.update_with_no_effect(self.runtime, f)
|
||||
self.id.update_with_no_effect(
|
||||
self.runtime,
|
||||
f,
|
||||
#[cfg(debug_assertions)]
|
||||
None,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1595,7 +1654,16 @@ impl<T> SignalUpdate<T> for RwSignal<T> {
|
||||
)]
|
||||
#[inline(always)]
|
||||
fn update(&self, f: impl FnOnce(&mut T)) {
|
||||
if self.id.update(self.runtime, f).is_none() {
|
||||
if self
|
||||
.id
|
||||
.update(
|
||||
self.runtime,
|
||||
f,
|
||||
#[cfg(debug_assertions)]
|
||||
Some(self.defined_at),
|
||||
)
|
||||
.is_none()
|
||||
{
|
||||
warn_updating_dead_signal(
|
||||
#[cfg(any(debug_assertions, feature = "ssr"))]
|
||||
self.defined_at,
|
||||
@@ -1618,7 +1686,12 @@ impl<T> SignalUpdate<T> for RwSignal<T> {
|
||||
)]
|
||||
#[inline(always)]
|
||||
fn try_update<O>(&self, f: impl FnOnce(&mut T) -> O) -> Option<O> {
|
||||
self.id.update(self.runtime, f)
|
||||
self.id.update(
|
||||
self.runtime,
|
||||
f,
|
||||
#[cfg(debug_assertions)]
|
||||
None,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1649,7 +1722,12 @@ impl<T> SignalSet<T> for RwSignal<T> {
|
||||
)
|
||||
)]
|
||||
fn set(&self, value: T) {
|
||||
self.id.update(self.runtime, |n| *n = value);
|
||||
self.id.update(
|
||||
self.runtime,
|
||||
|n| *n = value,
|
||||
#[cfg(debug_assertions)]
|
||||
Some(self.defined_at),
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
@@ -1668,8 +1746,12 @@ impl<T> SignalSet<T> for RwSignal<T> {
|
||||
fn try_set(&self, new_value: T) -> Option<T> {
|
||||
let mut new_value = Some(new_value);
|
||||
|
||||
self.id
|
||||
.update(self.runtime, |t| *t = new_value.take().unwrap());
|
||||
self.id.update(
|
||||
self.runtime,
|
||||
|t| *t = new_value.take().unwrap(),
|
||||
#[cfg(debug_assertions)]
|
||||
None,
|
||||
);
|
||||
|
||||
new_value
|
||||
}
|
||||
@@ -1950,6 +2032,9 @@ impl NodeId {
|
||||
&self,
|
||||
runtime: RuntimeId,
|
||||
f: impl FnOnce(&mut T) -> U,
|
||||
#[cfg(debug_assertions)] defined_at: Option<
|
||||
&'static std::panic::Location<'static>,
|
||||
>,
|
||||
) -> Option<U>
|
||||
where
|
||||
T: 'static,
|
||||
@@ -1968,14 +2053,21 @@ impl NodeId {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
debug_warn!(
|
||||
"[Signal::update] You’re trying to update a Signal<{}> \
|
||||
that has already been disposed of. This is probably \
|
||||
either a logic error in a component that creates and \
|
||||
disposes of scopes, or a Resource resolving after its \
|
||||
scope has been dropped without having been cleaned up.",
|
||||
std::any::type_name::<T>()
|
||||
);
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
if let Some(defined_at) = defined_at {
|
||||
debug_warn!(
|
||||
"[Signal::update] You’re trying to update a \
|
||||
Signal<{}> (defined at {defined_at}) that has \
|
||||
already been disposed of. This is probably \
|
||||
either a logic error in a component that creates \
|
||||
and disposes of scopes. If it does cause cause \
|
||||
any issues, it is safe to ignore this warning, \
|
||||
which occurs only in debug mode.",
|
||||
std::any::type_name::<T>()
|
||||
);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
})
|
||||
@@ -1987,6 +2079,9 @@ impl NodeId {
|
||||
&self,
|
||||
runtime_id: RuntimeId,
|
||||
f: impl FnOnce(&mut T) -> U,
|
||||
#[cfg(debug_assertions)] defined_at: Option<
|
||||
&'static std::panic::Location<'static>,
|
||||
>,
|
||||
) -> Option<U>
|
||||
where
|
||||
T: 'static,
|
||||
@@ -2005,14 +2100,21 @@ impl NodeId {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
debug_warn!(
|
||||
"[Signal::update] You’re trying to update a Signal<{}> \
|
||||
that has already been disposed of. This is probably \
|
||||
either a logic error in a component that creates and \
|
||||
disposes of scopes, or a Resource resolving after its \
|
||||
scope has been dropped without having been cleaned up.",
|
||||
std::any::type_name::<T>()
|
||||
);
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
if let Some(defined_at) = defined_at {
|
||||
debug_warn!(
|
||||
"[Signal::update] You’re trying to update a \
|
||||
Signal<{}> (defined at {defined_at}) that has \
|
||||
already been disposed of. This is probably \
|
||||
either a logic error in a component that creates \
|
||||
and disposes of scopes. If it does cause cause \
|
||||
any issues, it is safe to ignore this warning, \
|
||||
which occurs only in debug mode.",
|
||||
std::any::type_name::<T>()
|
||||
);
|
||||
}
|
||||
}
|
||||
None
|
||||
};
|
||||
|
||||
@@ -2034,12 +2136,20 @@ impl NodeId {
|
||||
&self,
|
||||
runtime: RuntimeId,
|
||||
f: impl FnOnce(&mut T) -> U,
|
||||
#[cfg(debug_assertions)] defined_at: Option<
|
||||
&'static std::panic::Location<'static>,
|
||||
>,
|
||||
) -> Option<U>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
// update the value
|
||||
self.update_value(runtime, f)
|
||||
self.update_value(
|
||||
runtime,
|
||||
f,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ readme = "../README.md"
|
||||
|
||||
[dependencies]
|
||||
leptos_reactive = { workspace = true }
|
||||
server_fn = { workspace = true}
|
||||
server_fn = { workspace = true }
|
||||
lazy_static = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
thiserror = "1"
|
||||
@@ -21,7 +21,6 @@ inventory = "0.3"
|
||||
leptos = { path = "../leptos" }
|
||||
|
||||
[features]
|
||||
default = ["default-tls"]
|
||||
csr = ["leptos_reactive/csr"]
|
||||
default-tls = ["server_fn/default-tls"]
|
||||
hydrate = ["leptos_reactive/hydrate"]
|
||||
|
||||
1
leptos_server/Makefile.toml
Normal file
1
leptos_server/Makefile.toml
Normal file
@@ -0,0 +1 @@
|
||||
extend = { path = "../cargo-make/main.toml" }
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_meta"
|
||||
version = "0.4.2"
|
||||
version = "0.4.3"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
@@ -12,7 +12,7 @@ cfg-if = "1"
|
||||
leptos = { workspace = true }
|
||||
tracing = "0.1"
|
||||
wasm-bindgen = "0.2"
|
||||
indexmap = "1"
|
||||
indexmap = "2"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
|
||||
1
meta/Makefile.toml
Normal file
1
meta/Makefile.toml
Normal file
@@ -0,0 +1 @@
|
||||
extend = { path = "../cargo-make/main.toml" }
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::use_head;
|
||||
use leptos::*;
|
||||
use leptos::{nonce::use_nonce, *};
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// Injects an [HTMLLinkElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement) into the document
|
||||
@@ -109,6 +109,7 @@ pub fn Link(
|
||||
.attr("title", title)
|
||||
.attr("type", type_)
|
||||
.attr("blocking", blocking)
|
||||
.attr("nonce", use_nonce(cx))
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::use_head;
|
||||
use leptos::*;
|
||||
use leptos::{nonce::use_nonce, *};
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// Injects an [HTMLScriptElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLScriptElement) into the document
|
||||
@@ -85,6 +85,7 @@ pub fn Script(
|
||||
.attr("src", src)
|
||||
.attr("type", type_)
|
||||
.attr("blocking", blocking)
|
||||
.attr("nonce", use_nonce(cx))
|
||||
}
|
||||
});
|
||||
let builder_el = if let Some(children) = children {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::use_head;
|
||||
use leptos::*;
|
||||
use leptos::{nonce::use_nonce, *};
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// Injects an [HTMLStyleElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLStyleElement) into the document
|
||||
@@ -57,6 +57,7 @@ pub fn Style(
|
||||
.attr("nonce", nonce)
|
||||
.attr("title", title)
|
||||
.attr("blocking", blocking)
|
||||
.attr("nonce", use_nonce(cx))
|
||||
}
|
||||
});
|
||||
let builder_el = if let Some(children) = children {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_router"
|
||||
version = "0.4.2"
|
||||
version = "0.4.3"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
|
||||
1
router/Makefile.toml
Normal file
1
router/Makefile.toml
Normal file
@@ -0,0 +1 @@
|
||||
extend = { path = "../cargo-make/main.toml" }
|
||||
@@ -215,7 +215,6 @@ where
|
||||
error.try_set(None);
|
||||
}
|
||||
if let Some(on_response) = on_response.clone() {
|
||||
leptos::log!("running on_response");
|
||||
on_response(resp.as_raw());
|
||||
}
|
||||
// Check all the logical 3xx responses that might
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::{use_location, use_resolved_path, State};
|
||||
use leptos::{leptos_dom::IntoView, *};
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// Describes a value that is either a static or a reactive URL, i.e.,
|
||||
/// a [String], a [&str], or a reactive `Fn() -> String`.
|
||||
@@ -56,6 +57,16 @@ pub fn A<H>(
|
||||
/// if false, link is marked active if the current route starts with it.
|
||||
#[prop(optional)]
|
||||
exact: bool,
|
||||
/// Provides a class to be added when the link is active. If provided, it will
|
||||
/// be added at the same time that the `aria-current` attribute is set.
|
||||
///
|
||||
/// This supports multiple space-separated class names.
|
||||
///
|
||||
/// **Performance**: If it’s possible to style the link using the CSS with the
|
||||
/// `[aria-current=page]` selector, you should prefer that, as it enables significant
|
||||
/// SSR optimizations.
|
||||
#[prop(optional, into)]
|
||||
active_class: Option<Cow<'static, str>>,
|
||||
/// An object of any type that will be pushed to router state
|
||||
#[prop(optional)]
|
||||
state: Option<State>,
|
||||
@@ -83,12 +94,13 @@ where
|
||||
cx: Scope,
|
||||
href: Memo<Option<String>>,
|
||||
exact: bool,
|
||||
state: Option<State>,
|
||||
replace: bool,
|
||||
#[allow(unused)] state: Option<State>,
|
||||
#[allow(unused)] replace: bool,
|
||||
class: Option<AttributeValue>,
|
||||
#[allow(unused)] active_class: Option<Cow<'static, str>>,
|
||||
id: Option<String>,
|
||||
children: Children,
|
||||
) -> HtmlElement<leptos::html::A> {
|
||||
) -> View {
|
||||
#[cfg(not(any(feature = "hydrate", feature = "csr")))]
|
||||
{
|
||||
_ = state;
|
||||
@@ -118,20 +130,86 @@ where
|
||||
}
|
||||
});
|
||||
|
||||
view! { cx,
|
||||
<a
|
||||
href=move || href.get().unwrap_or_default()
|
||||
prop:state={state.map(|s| s.to_js_value())}
|
||||
prop:replace={replace}
|
||||
aria-current=move || if is_active.get() { Some("page") } else { None }
|
||||
class=class
|
||||
id=id
|
||||
>
|
||||
{children(cx)}
|
||||
</a>
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
// if we have `active_class`, the SSR optimization doesn't play nicely
|
||||
// so we use the builder instead
|
||||
if let Some(active_class) = active_class {
|
||||
let mut a = leptos::html::a(cx)
|
||||
.attr("href", move || href.get().unwrap_or_default())
|
||||
.attr("aria-current", move || {
|
||||
if is_active.get() {
|
||||
Some("page")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.attr(
|
||||
"class",
|
||||
class.map(|class| class.into_attribute_boxed(cx)),
|
||||
);
|
||||
|
||||
for class_name in active_class.split_ascii_whitespace() {
|
||||
a = a.class(class_name.to_string(), move || is_active.get())
|
||||
}
|
||||
|
||||
a.attr("id", id).child(children(cx)).into_view(cx)
|
||||
}
|
||||
// but keep the nice SSR optimization in most cases
|
||||
else {
|
||||
view! { cx,
|
||||
<a
|
||||
href=move || href.get().unwrap_or_default()
|
||||
aria-current=move || if is_active.get() { Some("page") } else { None }
|
||||
class=class
|
||||
id=id
|
||||
>
|
||||
{children(cx)}
|
||||
</a>
|
||||
}
|
||||
.into_view(cx)
|
||||
}
|
||||
}
|
||||
|
||||
// the non-SSR version doesn't need the SSR optimizations
|
||||
// DRY here to avoid WASM binary size bloat
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
{
|
||||
let a = view! { cx,
|
||||
<a
|
||||
href=move || href.get().unwrap_or_default()
|
||||
prop:state={state.map(|s| s.to_js_value())}
|
||||
prop:replace={replace}
|
||||
aria-current=move || if is_active.get() { Some("page") } else { None }
|
||||
class=class
|
||||
id=id
|
||||
>
|
||||
{children(cx)}
|
||||
</a>
|
||||
};
|
||||
if let Some(active_class) = active_class {
|
||||
let mut a = a;
|
||||
for class_name in active_class.split_ascii_whitespace() {
|
||||
a = a.class(class_name.to_string(), move || is_active.get())
|
||||
}
|
||||
a
|
||||
} else {
|
||||
a
|
||||
}
|
||||
.into_view(cx)
|
||||
}
|
||||
}
|
||||
|
||||
let href = use_resolved_path(cx, move || href.to_href()());
|
||||
inner(cx, href, exact, state, replace, class, id, children)
|
||||
inner(
|
||||
cx,
|
||||
href,
|
||||
exact,
|
||||
state,
|
||||
replace,
|
||||
class,
|
||||
active_class,
|
||||
id,
|
||||
children,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -126,12 +126,16 @@ impl History for BrowserIntegration {
|
||||
.unwrap_or(hash);
|
||||
let el = leptos_dom::document().get_element_by_id(&hash);
|
||||
if let Some(el) = el {
|
||||
el.scroll_into_view()
|
||||
} else if loc.scroll {
|
||||
leptos_dom::window().scroll_to_with_x_and_y(0.0, 0.0);
|
||||
el.scroll_into_view();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// scroll to top
|
||||
if loc.scroll {
|
||||
leptos_dom::window().scroll_to_with_x_and_y(0.0, 0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,11 +28,10 @@ gloo-net = "0.2"
|
||||
js-sys = "0.3"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
reqwest = { version = "0.11"}
|
||||
reqwest = { version = "0.11", default-features = false }
|
||||
once_cell = "1"
|
||||
|
||||
[features]
|
||||
default = ["default-tls"]
|
||||
default-tls = ["reqwest/default-tls"]
|
||||
rustls = ["reqwest/rustls-tls"]
|
||||
ssr = ["inventory"]
|
||||
|
||||
1
server_fn/Makefile.toml
Normal file
1
server_fn/Makefile.toml
Normal file
@@ -0,0 +1 @@
|
||||
extend = { path = "../cargo-make/main.toml" }
|
||||
4
server_fn/server_fn_macro_default/Makefile.toml
Normal file
4
server_fn/server_fn_macro_default/Makefile.toml
Normal file
@@ -0,0 +1,4 @@
|
||||
extend = { path = "../../cargo-make/main.toml" }
|
||||
|
||||
[tasks.check-format]
|
||||
env = { LEPTOS_PROJECT_DIRECTORY = "../../" }
|
||||
@@ -608,6 +608,12 @@ where
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let binary = binary.as_ref();
|
||||
|
||||
if status == 400 {
|
||||
return Err(ServerFnError::ServerError(
|
||||
"No server function was found at this URL.".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
ciborium::de::from_reader(binary)
|
||||
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
|
||||
} else {
|
||||
@@ -616,6 +622,10 @@ where
|
||||
.await
|
||||
.map_err(|e| ServerFnError::Deserialization(e.to_string()))?;
|
||||
|
||||
if status == 400 {
|
||||
return Err(ServerFnError::ServerError(text));
|
||||
}
|
||||
|
||||
let mut deserializer = JSONDeserializer::from_str(&text);
|
||||
T::deserialize(&mut deserializer)
|
||||
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
|
||||
|
||||
1
server_fn_macro/Makefile.toml
Normal file
1
server_fn_macro/Makefile.toml
Normal file
@@ -0,0 +1 @@
|
||||
extend = { path = "../cargo-make/main.toml" }
|
||||
Reference in New Issue
Block a user