Compare commits

..

28 Commits

Author SHA1 Message Date
Greg Johnston
2aa14e5e65 differentiate suspense/errorboundary 2023-07-15 16:35:08 -04:00
Greg Johnston
40f6ed9252 make component key paths independent 2023-07-15 12:59:12 -04:00
Greg Johnston
9d9ed45565 make hydration keys !Copy 2023-07-15 10:47:27 -04:00
Greg Johnston
8e68699435 feat: add support for adding CSP nonces (#1348) 2023-07-14 16:37:18 -04:00
Greg Johnston
77580401da fix: hydration-key conflicts between <ErrorBoundary/> children and fallback (#1354) 2023-07-14 14:37:54 -04:00
Joseph Cruz
7902e7edb7 ci: speed up verification (#1347)
* build: introduce ci task
* refactor(ci): rename cargo make task runner
* ci: add ci workflow
* ci: remove redundant workflows
2023-07-14 14:37:17 -04:00
Greg Johnston
4ad223277d fix: duplicated meta content during async rendering (#1352) 2023-07-14 13:14:19 -04:00
Greg Johnston
6f5da11c72 docs/warnings: don't warn when a resource resolves after its scope has been disposed (#1351) 2023-07-14 13:09:57 -04:00
Greg Johnston
3eed86fbf3 docs/warnings: improve ServerFnError when a server function is not found (#1350) 2023-07-14 12:43:08 -04:00
Greg Johnston
10d51a854a v0.4.3 2023-07-14 09:22:19 -04:00
CircuitSacul
6c60bad757 examples: use cfg_attr for conditional derives (#1349)
Revert "use cfg_attr for conditional derives"

This reverts commit b5c85e9ed8a84e5a49f119ae8436d78f2bee1fea.

Revert "Revert "use cfg_attr for conditional derives""

This reverts commit b538256cd69bc2321cdc9066441d71fc94ed80b9.
2023-07-14 09:20:55 -04:00
g-re-g
79f666b5da docs: don't run rust code snippets and update getting started (#1346)
* disable running leptos examples

* typo and small language changes to 02_getting_started
2023-07-14 07:10:59 -04:00
Greg Johnston
3ea3a40395 fix: server_fn rustls feature shouldn't pull in default-tls (#1343) 2023-07-13 19:41:55 -04:00
Greg Johnston
193aa79956 docs: how not to mutate the DOM during rendering (#1344) 2023-07-13 16:57:07 -04:00
Joseph Cruz
3481a6ee53 build: run tasks from workpace or member directory (#1339) 2023-07-13 16:46:51 -04:00
Greg Johnston
d1ef5fce9f fix: un-register <Suspense/> from resource reads when <Suspense/> is unmounted (#1342) 2023-07-13 14:42:05 -04:00
Greg Johnston
5d48911f01 fix: check LEPTOS_OUTPUT_NAME correctly at compile time (#1338) 2023-07-13 10:49:13 -04:00
Greg Johnston
8a90f97959 fix: routing logic to scroll to top was broken (#1335) 2023-07-13 06:43:49 -04:00
Greg Johnston
e9665b34e5 feat: add active_class prop on <A/> (#1323) 2023-07-12 16:21:07 -04:00
Greg Johnston
d4b1ceda90 fix: event delegation issue with <input name="host"> (#1332) 2023-07-12 16:20:11 -04:00
Greg Johnston
a0fae88f7d docs: clarify nightly in "Getting Started" (#1330) 2023-07-12 11:56:45 -04:00
Greg Johnston
03a8609680 fix: warning generated by new #[must_use] on views (#1329) 2023-07-12 10:00:05 -04:00
g-re-g
3e40f9cc66 chore: bump indexmap to 2 (#1325) 2023-07-11 15:24:21 -04:00
Greg Johnston
576bb078f7 fix: Actix server fn redirect() duplicate Location headers (#1326) 2023-07-11 13:57:44 -04:00
Filip Dutescu
3cdcc85c87 feat(config): implement common traits for Env (#1324)
In order to facilitate its usage in other structs, which might derive
various traits, such as `serde::Serialize` or `PartialEq`, implement
them for the `leptos_config::Env` enum.

Signed-off-by: Filip Dutescu <filip@hucksy.dev>
2023-07-11 09:37:42 -04:00
Greg Johnston
ec3a26dfbc fix: <ActionForm/> should set value even if redirected (#1321) 2023-07-11 09:37:13 -04:00
Filip Dutescu
c755dae6ee feat(leptos-config): kebab-case via serde's rename_all (#1308)
Currently, `leptos::LeptosOptions` are deserialized (and serialized)
to `snake_case`, since, by default, `serde` uses the name of the field
as-is. The options given via `.toml` files are given using `kebab-case`.
This behaviour leads to problems if you wish to use
`leptos::LeptosOptions` as the type of a field in your own config,
which uses the `kebab-case` syntax, as it will not provide any values
to that field.

These changes switch out the previous mechanism of manually replacing
the `-` character to the `_` character in order for serialization to
work. In its place, it uses `serde`'s `#[serde(rename_all = ...)]`
macro to handle the naming convention.

In case other naming conventions are used, a workaround would be to
define a user-owned mirror struct of `leptos::LeptosOptions`, which
would contain the required fields, deserialize it as desired and'
convert it to the latter via, for example, the `From<T>` trait.

Closes: #1295

Signed-off-by: Filip Dutescu <filip@hucksy.dev>
2023-07-11 09:36:55 -04:00
Greg Johnston
b67d51e019 docs: remove empty chapter from book 2023-07-10 07:38:12 -04:00
78 changed files with 1071 additions and 597 deletions

View File

@@ -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"

View File

@@ -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
View 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"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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"

View File

@@ -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

View File

@@ -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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,2 @@
[output.html.playground]
runnable = false

View File

@@ -14,31 +14,40 @@ If you dont 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 youd rather use stable Rust with Leptos, you can do that too. In the guide and examples, youll 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

View File

@@ -1 +0,0 @@
# Responding to Changes with create_effect

View File

@@ -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)

View File

@@ -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
Its pretty rare that you do this intentionally, but it could happen from somehow running different logic on the server and in the browser. If youre seeing warnings like this and you dont think its your fault, its much more likely that its 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 youve been used to using to make requests in the browser, and use it in a `create_resource` in a server-rendered app.

View File

@@ -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,
}
}
}

View File

@@ -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,
}
}
}

View File

@@ -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,
}
}
}

View File

@@ -19,3 +19,6 @@ serde_json = "1"
parking_lot = "0.12.1"
regex = "1.7.0"
tracing = "0.1.37"
[features]
nonce = ["leptos/nonce"]

View File

@@ -0,0 +1,4 @@
extend = { path = "../../cargo-make/main.toml" }
[tasks.check-format]
env = { LEPTOS_PROJECT_DIRECTORY = "../../" }

View File

@@ -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() })

View File

@@ -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"]

View File

@@ -0,0 +1,4 @@
extend = { path = "../../cargo-make/main.toml" }
[tasks.check-format]
env = { LEPTOS_PROJECT_DIRECTORY = "../../" }

View File

@@ -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

View File

@@ -0,0 +1,4 @@
extend = { path = "../../cargo-make/main.toml" }
[tasks.check-format]
env = { LEPTOS_PROJECT_DIRECTORY = "../../" }

View File

@@ -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}")
}

View File

@@ -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"]

View File

@@ -0,0 +1,4 @@
extend = { path = "../../cargo-make/main.toml" }
[tasks.check-format]
env = { LEPTOS_PROJECT_DIRECTORY = "../../" }

View File

@@ -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

View File

@@ -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",

View File

@@ -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"]

View File

@@ -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"))]
{

View File

@@ -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.

View File

@@ -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)
}

View File

@@ -289,4 +289,4 @@ fn None(cx: Scope) -> impl IntoView {
</div>
</div>
}
}
}

View File

@@ -0,0 +1 @@
extend = { path = "../cargo-make/main.toml" }

View File

@@ -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))

View File

@@ -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, ".");

View File

@@ -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
View File

@@ -0,0 +1 @@
extend = { path = "../cargo-make/main.toml" }

View File

@@ -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()
}
}

View File

@@ -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();

View File

@@ -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()
})
}

View File

@@ -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 {

View File

@@ -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
View 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())
}
}

View File

@@ -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)

View File

@@ -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>| {

View File

@@ -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();

View File

@@ -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"

View File

@@ -0,0 +1 @@
extend = { path = "../cargo-make/main.toml" }

View 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

View File

@@ -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,

View File

@@ -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]

View File

@@ -0,0 +1 @@
extend = { path = "../cargo-make/main.toml" }

View File

@@ -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()

View File

@@ -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] Youre 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] Youre 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] Youre 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] Youre 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,
)
}
}

View File

@@ -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"]

View File

@@ -0,0 +1 @@
extend = { path = "../cargo-make/main.toml" }

View File

@@ -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
View File

@@ -0,0 +1 @@
extend = { path = "../cargo-make/main.toml" }

View File

@@ -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))
}
});

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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
View File

@@ -0,0 +1 @@
extend = { path = "../cargo-make/main.toml" }

View File

@@ -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

View File

@@ -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 its 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,
)
}

View File

@@ -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);
}
}
}

View File

@@ -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
View File

@@ -0,0 +1 @@
extend = { path = "../cargo-make/main.toml" }

View File

@@ -0,0 +1,4 @@
extend = { path = "../../cargo-make/main.toml" }
[tasks.check-format]
env = { LEPTOS_PROJECT_DIRECTORY = "../../" }

View File

@@ -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()))

View File

@@ -0,0 +1 @@
extend = { path = "../cargo-make/main.toml" }