mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 15:44:42 -05:00
Compare commits
30 Commits
eager-clon
...
v0.5.7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c4cf5471d | ||
|
|
dd0c349554 | ||
|
|
dd5a0ae094 | ||
|
|
5cacb57283 | ||
|
|
b356d3cd28 | ||
|
|
ae1de88916 | ||
|
|
67dd188358 | ||
|
|
1d4772251a | ||
|
|
98f18e7c31 | ||
|
|
3f34d9bb23 | ||
|
|
44db400f6b | ||
|
|
3a5730800c | ||
|
|
041b86e6e5 | ||
|
|
d71feada7e | ||
|
|
1eaf886481 | ||
|
|
c7da3998ba | ||
|
|
10bbeea697 | ||
|
|
2d70229608 | ||
|
|
bbcef811f4 | ||
|
|
f5bf539148 | ||
|
|
855a3c65c0 | ||
|
|
16cf3c4eaf | ||
|
|
da533ad4e0 | ||
|
|
b4a9db51be | ||
|
|
15946c6136 | ||
|
|
9f4c480725 | ||
|
|
74f9c3cd82 | ||
|
|
34fb39c976 | ||
|
|
ea80a21a54 | ||
|
|
33590d487b |
@@ -26,7 +26,7 @@ jobs:
|
||||
|
||||
- name: Get example project directories that changed
|
||||
id: changed-dirs
|
||||
uses: tj-actions/changed-files@v39
|
||||
uses: tj-actions/changed-files@v41
|
||||
with:
|
||||
dir_names: true
|
||||
dir_names_max_depth: "2"
|
||||
|
||||
4
.github/workflows/get-example-changed.yml
vendored
4
.github/workflows/get-example-changed.yml
vendored
@@ -21,10 +21,10 @@ jobs:
|
||||
|
||||
- name: Get example files that changed
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v39
|
||||
uses: tj-actions/changed-files@v41
|
||||
with:
|
||||
files: |
|
||||
examples
|
||||
examples/**
|
||||
!examples/cargo-make
|
||||
!examples/gtk
|
||||
!examples/Makefile.toml
|
||||
|
||||
26
.github/workflows/get-leptos-changed.yml
vendored
26
.github/workflows/get-leptos-changed.yml
vendored
@@ -19,21 +19,21 @@ jobs:
|
||||
|
||||
- name: Get source files that changed
|
||||
id: changed-source
|
||||
uses: tj-actions/changed-files@v39
|
||||
uses: tj-actions/changed-files@v41
|
||||
with:
|
||||
files: |
|
||||
integrations
|
||||
leptos
|
||||
leptos_config
|
||||
leptos_dom
|
||||
leptos_hot_reload
|
||||
leptos_macro
|
||||
leptos_reactive
|
||||
leptos_server
|
||||
meta
|
||||
router
|
||||
server_fn
|
||||
server_fn_macro
|
||||
integrations/**
|
||||
leptos/**
|
||||
leptos_config/**
|
||||
leptos_dom/**
|
||||
leptos_hot_reload/**
|
||||
leptos_macro/**
|
||||
leptos_reactive/**
|
||||
leptos_server/**
|
||||
meta/**
|
||||
router/**
|
||||
server_fn/**
|
||||
server_fn_macro/**
|
||||
|
||||
- name: List source files that changed
|
||||
run: echo '${{ steps.changed-source.outputs.all_changed_files }}'
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -10,4 +10,5 @@ Cargo.lock
|
||||
.direnv
|
||||
.envrc
|
||||
|
||||
.vscode
|
||||
.vscode
|
||||
vendor
|
||||
28
Cargo.toml
28
Cargo.toml
@@ -26,22 +26,22 @@ members = [
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.5.4"
|
||||
version = "0.5.7"
|
||||
|
||||
[workspace.dependencies]
|
||||
leptos = { path = "./leptos", version = "0.5.4" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.5.4" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.5.4" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.5.4" }
|
||||
leptos_reactive = { path = "./leptos_reactive", version = "0.5.4" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.5.4" }
|
||||
server_fn = { path = "./server_fn", version = "0.5.4" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.5.4" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.5.4" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.5.4" }
|
||||
leptos_router = { path = "./router", version = "0.5.4" }
|
||||
leptos_meta = { path = "./meta", version = "0.5.4" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.5.4" }
|
||||
leptos = { path = "./leptos", version = "0.5.7" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.5.7" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.5.7" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.5.7" }
|
||||
leptos_reactive = { path = "./leptos_reactive", version = "0.5.7" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.5.7" }
|
||||
server_fn = { path = "./server_fn", version = "0.5.7" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.5.7" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.5.7" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.5.7" }
|
||||
leptos_router = { path = "./router", version = "0.5.7" }
|
||||
leptos_meta = { path = "./meta", version = "0.5.7" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.5.7" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -4,30 +4,31 @@
|
||||
|
||||
The examples in this directory are all built and tested against the current `main` branch.
|
||||
|
||||
To the extent that new features have been released or breaking changes have been made since the previous release, the examples are compatible with the `main` branch but not the current release.
|
||||
|
||||
To see the examples as they were at the time of the `0.5.0` release, [click here](https://github.com/leptos-rs/leptos/tree/v0.5.0/examples).
|
||||
|
||||
## Cargo Make
|
||||
|
||||
[Cargo Make](https://sagiegurari.github.io/cargo-make/) is used to build, test, and run examples.
|
||||
|
||||
Here are the highlights.
|
||||
|
||||
- Extendable custom task files are located in the [cargo-make](./cargo-make/) directory
|
||||
- Running a task will automatically install `cargo` dependencies
|
||||
- Each `Makefile.toml` file must extend the [cargo-make/main.toml](./cargo-make/main.toml) file
|
||||
- [cargo-make](./cargo-make/) files that end in `*-test.toml` configure web testing strategies
|
||||
- Run `cargo make test-report` to learn which examples have web tests
|
||||
To the extent that new features have been released or breaking changes have been made since the previous release, the examples are compatible with the `main` branch and not the current release.
|
||||
|
||||
## Getting Started
|
||||
|
||||
The simplest way to get started with any example is to use the “quick start” command found in the README for each example. Most of the examples use either [`trunk`](https://trunkrs.dev/) (a simple build system and dev server for client-side-rendered apps) or [`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos) (a build system for server-rendered and client-hydrated apps).
|
||||
|
||||
## Using Cargo Make
|
||||
|
||||
You can also run any of the examples using [`cargo-make`](https://github.com/sagiegurari/cargo-make). Note that this is completely optional. We use it for CI, and it can be convenient for running the examples, but is not required.
|
||||
|
||||
Follow these steps to get any example up and running.
|
||||
|
||||
1. `cd` to the example root directory
|
||||
2. Run `cargo make ci` to setup and test the example
|
||||
3. Run `cargo make start` to run the example
|
||||
4. Open the client URL in the console output (<http://127.0.0.1:8080> or <http://127.0.0.1:3000> by default)
|
||||
5. Run `cargo make stop` to end any processes started by `cargo make start`.
|
||||
|
||||
Here are a few additional notes:
|
||||
|
||||
- Extendable custom task files are located in the [cargo-make](./cargo-make/) directory
|
||||
- Running a task will automatically install `cargo` dependencies
|
||||
- Each `Makefile.toml` file must extend the [cargo-make/main.toml](./cargo-make/main.toml) file
|
||||
- [cargo-make](./cargo-make/) files that end in `*-test.toml` configure web testing strategies
|
||||
- Run `cargo make test-report` to learn which examples have web tests
|
||||
|
||||
## Prerequisites
|
||||
|
||||
|
||||
2
examples/counter/rust-toolchain.toml
Normal file
2
examples/counter/rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
2
examples/counter_url_query/rust-toolchain.toml
Normal file
2
examples/counter_url_query/rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
@@ -1,3 +1,2 @@
|
||||
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
|
||||
2
examples/error_boundary/rust-toolchain.toml
Normal file
2
examples/error_boundary/rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
@@ -5,3 +5,7 @@ This example demonstrates how Leptos Errors can work with an Axum backend on a s
|
||||
## Getting Started
|
||||
|
||||
See the [Examples README](../README.md) for setup and run instructions.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Run `cargo leptos watch` to run this example.
|
||||
|
||||
2
examples/errors_axum/rust-toolchain.toml
Normal file
2
examples/errors_axum/rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
2
examples/fetch/rust-toolchain.toml
Normal file
2
examples/fetch/rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
2
examples/hackernews/rust-toolchain.toml
Normal file
2
examples/hackernews/rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
2
examples/hackernews_axum/rust-toolchain.toml
Normal file
2
examples/hackernews_axum/rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
2
examples/hackernews_islands_axum/rust-toolchain.toml
Normal file
2
examples/hackernews_islands_axum/rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
2
examples/js-framework-benchmark/rust-toolchain.toml
Normal file
2
examples/js-framework-benchmark/rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
@@ -1,3 +1,2 @@
|
||||
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
|
||||
@@ -5,6 +5,10 @@ test.describe("Test Router example", () => {
|
||||
await page.goto("/");
|
||||
});
|
||||
|
||||
test("Starts on correct home page", async({ page }) => {
|
||||
await expect(page.getByText("Select a contact.")).toBeVisible();
|
||||
});
|
||||
|
||||
const links = [
|
||||
{ label: "Bill Smith", url: "/0" },
|
||||
{ label: "Tim Jones", url: "/1" },
|
||||
|
||||
2
examples/router/rust-toolchain.toml
Normal file
2
examples/router/rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
2
examples/session_auth_axum/rust-toolchain.toml
Normal file
2
examples/session_auth_axum/rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
2
examples/slots/rust-toolchain.toml
Normal file
2
examples/slots/rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
110
examples/sso_auth_axum/Cargo.toml
Normal file
110
examples/sso_auth_axum/Cargo.toml
Normal file
@@ -0,0 +1,110 @@
|
||||
[package]
|
||||
name = "sso_auth_axum"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
oauth2 = {version="4.4.2",optional=true}
|
||||
anyhow = "1.0.66"
|
||||
console_log = "1.0.0"
|
||||
rand = { version = "0.8.5", features = ["min_const_gen"], optional = true }
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.25"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { path = "../../leptos"}
|
||||
leptos_meta = { path = "../../meta" }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_router = { path = "../../router"}
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
serde_json = {version="1.0.108", optional = true }
|
||||
axum = { version = "0.6.1", optional = true, features=["macros"] }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.4", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.22.0", features = ["full"], optional = true }
|
||||
http = { version = "0.2.8" }
|
||||
sqlx = { version = "0.7", features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
], optional = true }
|
||||
thiserror = "1.0.38"
|
||||
wasm-bindgen = "0.2"
|
||||
axum_session_auth = { version = "0.9", features = [
|
||||
"sqlite-rustls",
|
||||
], optional = true }
|
||||
axum_session = { version = "0.9", features = [
|
||||
"sqlite-rustls",
|
||||
], optional = true }
|
||||
async-trait = { version = "0.1.64", optional = true }
|
||||
reqwest= {version="0.11",optional=true, features=["json"]}
|
||||
|
||||
[features]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:serde_json",
|
||||
"dep:axum",
|
||||
"dep:tower",
|
||||
"dep:tower-http",
|
||||
"dep:tokio",
|
||||
"dep:reqwest",
|
||||
"dep:oauth2",
|
||||
"dep:axum_session_auth",
|
||||
"dep:axum_session",
|
||||
"dep:async-trait",
|
||||
"dep:sqlx",
|
||||
"dep:rand",
|
||||
"leptos/ssr",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
"dep:leptos_axum",
|
||||
]
|
||||
|
||||
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
output-name = "sso_auth_axum"
|
||||
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
|
||||
site-root = "target/site"
|
||||
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
|
||||
# Defaults to pkg
|
||||
site-pkg-dir = "pkg"
|
||||
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
|
||||
style-file = "./style.css"
|
||||
# [Optional] Files in the asset-dir will be copied to the site-root directory
|
||||
assets-dir = "public"
|
||||
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
|
||||
site-addr = "127.0.0.1:3000"
|
||||
# The port to use for automatic reload monitoring
|
||||
reload-port = 3001
|
||||
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
||||
end2end-cmd = "npx playwright test"
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
browserquery = "defaults"
|
||||
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
|
||||
watch = false
|
||||
# The environment Leptos will run in, usually either "DEV" or "PROD"
|
||||
env = "DEV"
|
||||
# The features to use when compiling the bin target
|
||||
#
|
||||
# Optional. Can be over-ridden with the command line parameter --bin-features
|
||||
bin-features = ["ssr"]
|
||||
|
||||
# If the --no-default-features flag should be used when compiling the bin target
|
||||
#
|
||||
# Optional. Defaults to false.
|
||||
bin-default-features = false
|
||||
|
||||
# The features to use when compiling the lib target
|
||||
#
|
||||
# Optional. Can be over-ridden with the command line parameter --lib-features
|
||||
lib-features = ["hydrate"]
|
||||
|
||||
# If the --no-default-features flag should be used when compiling the lib target
|
||||
#
|
||||
# Optional. Defaults to false.
|
||||
lib-default-features = false
|
||||
21
examples/sso_auth_axum/LICENSE
Normal file
21
examples/sso_auth_axum/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Greg Johnston
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
11
examples/sso_auth_axum/Makefile.toml
Normal file
11
examples/sso_auth_axum/Makefile.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
extend = { path = "../cargo-make/main.toml" }
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
83
examples/sso_auth_axum/README.md
Normal file
83
examples/sso_auth_axum/README.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# Leptos SSO Authenticated Email Display App with Axum
|
||||
|
||||
## Overview
|
||||
This project demonstrates various methods of implementing Single Sign-On (SSO) authorization using OAuth, specifically with the OAuth2 library. The primary focus is on the Authorization Code Grant flow.
|
||||
|
||||
### Process Flow
|
||||
1. **Initiating Sign-In:** When a user clicks the 'Sign In With {THIRD PARTY SERVICE}' button, the request is sent to a server function. This function retrieves an authorization URL from the third-party service.
|
||||
|
||||
2. **CSRF Token Handling:** During the URL fetch, a CSRF_TOKEN is generated and confirmed by the service to mitigate Cross-Site Request Forgery attacks. Learn more about CSRF [here](https://en.wikipedia.org/wiki/Cross-site_request_forgery). This token is stored on our server.
|
||||
|
||||
3. **User Redirection:** Post-login, users are redirected to our server with a URL formatted as follows:
|
||||
`http://your-redirect-uri.com/callback?code=AUTHORIZATION_CODE&state=CSRF_TOKEN`
|
||||
Note: Additional parameters like Scope and Client_ID may be included by the service.
|
||||
|
||||
4. **Token Acquisition:** The 'code' parameter in the URL is not the actual service token. Instead, it's used to fetch the token. We verify the CSRF_TOKEN in the URL against our server's stored token for security.
|
||||
|
||||
5. **Access Token Usage:** With a valid CSRF_TOKEN, we use the AUTHORIZATION_CODE in an HTTP Request to the third-party service. The response typically includes:
|
||||
- An `access token`
|
||||
- An `expires_in` value (time in seconds until token expiration)
|
||||
- A `refresh token` (used to renew the access token)
|
||||
|
||||
6. **Email Retrieval and Display:** The access token allows us to retrieve the user's email. This email is then displayed in our Email Display App.
|
||||
|
||||
7. **Session Management:** The `expires_in` value is sent to the client. The client uses this to set a timeout, ensuring that if the session is still active (the window hasn't been closed), it automatically triggers a token refresh when required.
|
||||
|
||||
|
||||
|
||||
## Client Side Rendering
|
||||
This example cannot be built as a trunk standalone CSR-only app. Only the server may directly connect to the database.
|
||||
|
||||
## Server Side Rendering with cargo-leptos
|
||||
cargo-leptos is now the easiest and most featureful way to build server side rendered apps with hydration. It provides automatic recompilation of client and server code, wasm optimisation, CSS minification, and more! Check out more about it [here](https://github.com/akesson/cargo-leptos)
|
||||
|
||||
## Env Vars
|
||||
Commands that run the program, cargo leptos watch, cargo leptos serve, cargo run etc... All need the following Environment variables
|
||||
G_AUTH_CLIENT_ID : This is the client ID given to you by google.
|
||||
G_AUTH_SECRET : This is the secret given to you by google.
|
||||
NGROK : this is the ngrok endpoint you get when you run ngrok http 3000
|
||||
|
||||
## Ngrok Google Set Up
|
||||
After running your app, run
|
||||
```bash
|
||||
ngrok http 3000
|
||||
```
|
||||
Then use google api's and services, go to credentials, create credentials, add your app name, and use the ngrok url as the origin
|
||||
and use the ngrok url with /g_auth as the redirect url. That will look like this `https://362b-24-34-20-189.ngrok-free.app/g_auth`
|
||||
Save you client ID and secret given to you by google. Use them as Envars when you run the program as below
|
||||
```bash
|
||||
REDIRECT_URL={ngrok_redirect_url} G_AUTH_CLIENT_ID={google_credential_client_id} G_AUTH_SECRET={google_credential_secret} {your command here...}
|
||||
```
|
||||
|
||||
1. Install cargo-leptos
|
||||
```bash
|
||||
cargo install --locked cargo-leptos
|
||||
```
|
||||
2. Build the site in watch mode, recompiling on file changes
|
||||
```bash
|
||||
cargo leptos watch
|
||||
```
|
||||
|
||||
Open browser on [http://localhost:3000/](http://localhost:3000/)
|
||||
|
||||
3. When ready to deploy, run
|
||||
```bash
|
||||
cargo leptos build --release
|
||||
```
|
||||
|
||||
## Server Side Rendering without cargo-leptos
|
||||
To run it as a server side app with hydration, you'll need to have wasm-pack installed.
|
||||
|
||||
0. Edit the `[package.metadata.leptos]` section and set `site-root` to `"."`. You'll also want to change the path of the `<StyleSheet / >` component in the root component to point towards the CSS file in the root. This tells leptos that the WASM/JS files generated by wasm-pack are available at `./pkg` and that the CSS files are no longer processed by cargo-leptos. Building to alternative folders is not supported at this time. You'll also want to edit the call to `get_configuration()` to pass in `Some(Cargo.toml)`, so that Leptos will read the settings instead of cargo-leptos. If you do so, your file/folder names cannot include dashes.
|
||||
1. Install wasm-pack
|
||||
```bash
|
||||
cargo install wasm-pack
|
||||
```
|
||||
2. Build the Webassembly used to hydrate the HTML from the server
|
||||
```bash
|
||||
wasm-pack build --target=web --debug --no-default-features --features=hydrate
|
||||
```
|
||||
3. Run the server to serve the Webassembly, JS, and HTML
|
||||
```bash
|
||||
cargo run --no-default-features --features=ssr
|
||||
```
|
||||
116
examples/sso_auth_axum/flake.lock
generated
Normal file
116
examples/sso_auth_axum/flake.lock
generated
Normal file
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1681202837,
|
||||
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"inputs": {
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1681202837,
|
||||
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1672580127,
|
||||
"narHash": "sha256-3lW3xZslREhJogoOkjeZtlBtvFMyxHku7I/9IVehhT8=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "0874168639713f547c05947c76124f78441ea46c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-22.05",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1681525152,
|
||||
"narHash": "sha256-KzI+ILcmU03iFWtB+ysPqtNmp8TP8v1BBReTuPP8MJY=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "b6f8d87208336d7cb85003b2e439fc707c38f92a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
37
examples/sso_auth_axum/flake.nix
Normal file
37
examples/sso_auth_axum/flake.nix
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-22.05";
|
||||
inputs.rust-overlay.url = "github:oxalica/rust-overlay";
|
||||
inputs.rust-overlay.inputs.nixpkgs.follows = "nixpkgs";
|
||||
inputs.flake-utils.url = "github:numtide/flake-utils";
|
||||
|
||||
outputs = { self, nixpkgs, rust-overlay, flake-utils }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [ (import rust-overlay) ];
|
||||
};
|
||||
in
|
||||
with pkgs; rec {
|
||||
devShells.default = mkShell {
|
||||
|
||||
shellHook = ''
|
||||
export PKG_CONFIG_PATH="${pkgs.openssl.dev}/lib/pkgconfig";
|
||||
'';
|
||||
|
||||
nativeBuildInputs = [
|
||||
pkg-config
|
||||
];
|
||||
buildInputs = [
|
||||
trunk
|
||||
sqlite
|
||||
sass
|
||||
openssl
|
||||
(rust-bin.nightly.latest.default.override {
|
||||
extensions = [ "rust-src" ];
|
||||
targets = [ "wasm32-unknown-unknown" ];
|
||||
})
|
||||
];
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_permissions (
|
||||
user_id INTEGER NOT NULL,
|
||||
token TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS csrf_tokens (
|
||||
csrf_token TEXT NOT NULL PRIMARY KEY UNIQUE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS google_tokens (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL UNIQUE,
|
||||
access_secret TEXT NOT NULL,
|
||||
refresh_secret TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) CONFLICT REPLACE
|
||||
);
|
||||
|
||||
BIN
examples/sso_auth_axum/public/favicon.ico
Normal file
BIN
examples/sso_auth_axum/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
144
examples/sso_auth_axum/src/auth.rs
Normal file
144
examples/sso_auth_axum/src/auth.rs
Normal file
@@ -0,0 +1,144 @@
|
||||
use cfg_if::cfg_if;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use sqlx::SqlitePool;
|
||||
use axum_session_auth::{SessionSqlitePool, Authentication, HasPermission};
|
||||
pub type AuthSession = axum_session_auth::AuthSession<User, i64, SessionSqlitePool, SqlitePool>;
|
||||
}}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct User {
|
||||
pub id: i64,
|
||||
pub email: String,
|
||||
pub permissions: HashSet<String>,
|
||||
}
|
||||
|
||||
impl Default for User {
|
||||
fn default() -> Self {
|
||||
let permissions = HashSet::new();
|
||||
|
||||
Self {
|
||||
id: -1,
|
||||
email: "example@example.com".into(),
|
||||
permissions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use async_trait::async_trait;
|
||||
|
||||
impl User {
|
||||
pub async fn get(id: i64, pool: &SqlitePool) -> Option<Self> {
|
||||
let sqluser = sqlx::query_as::<_, SqlUser>("SELECT * FROM users WHERE id = ?")
|
||||
.bind(id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
//lets just get all the tokens the user can use, we will only use the full permissions if modifing them.
|
||||
let sql_user_perms = sqlx::query_as::<_, SqlPermissionTokens>(
|
||||
"SELECT token FROM user_permissions WHERE user_id = ?;",
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
Some(sqluser.into_user(Some(sql_user_perms)))
|
||||
}
|
||||
|
||||
pub async fn get_from_email(email: &str, pool: &SqlitePool) -> Option<Self> {
|
||||
let sqluser = sqlx::query_as::<_, SqlUser>("SELECT * FROM users WHERE email = ?")
|
||||
.bind(email)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
//lets just get all the tokens the user can use, we will only use the full permissions if modifing them.
|
||||
let sql_user_perms = sqlx::query_as::<_, SqlPermissionTokens>(
|
||||
"SELECT token FROM user_permissions WHERE user_id = ?;",
|
||||
)
|
||||
.bind(sqluser.id)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
Some(sqluser.into_user(Some(sql_user_perms)))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Clone)]
|
||||
pub struct SqlPermissionTokens {
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Clone)]
|
||||
pub struct SqlCsrfToken {
|
||||
pub csrf_token: String,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Authentication<User, i64, SqlitePool> for User {
|
||||
async fn load_user(userid: i64, pool: Option<&SqlitePool>) -> Result<User, anyhow::Error> {
|
||||
let pool = pool.unwrap();
|
||||
|
||||
User::get(userid, pool)
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("Cannot get user"))
|
||||
}
|
||||
|
||||
fn is_authenticated(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn is_active(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn is_anonymous(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl HasPermission<SqlitePool> for User {
|
||||
async fn has(&self, perm: &str, _pool: &Option<&SqlitePool>) -> bool {
|
||||
self.permissions.contains(perm)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Clone)]
|
||||
pub struct SqlUser {
|
||||
pub id: i64,
|
||||
pub email: String,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Clone)]
|
||||
pub struct SqlRefreshToken {
|
||||
pub secret: String,
|
||||
}
|
||||
|
||||
|
||||
impl SqlUser {
|
||||
pub fn into_user(self, sql_user_perms: Option<Vec<SqlPermissionTokens>>) -> User {
|
||||
User {
|
||||
id: self.id,
|
||||
email: self.email,
|
||||
permissions: if let Some(user_perms) = sql_user_perms {
|
||||
user_perms
|
||||
.into_iter()
|
||||
.map(|x| x.token)
|
||||
.collect::<HashSet<String>>()
|
||||
} else {
|
||||
HashSet::<String>::new()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
24
examples/sso_auth_axum/src/error_template.rs
Normal file
24
examples/sso_auth_axum/src/error_template.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
use leptos::{view, Errors, For, IntoView, RwSignal, SignalGet, View};
|
||||
|
||||
// A basic function to display errors served by the error boundaries. Feel free to do more complicated things
|
||||
// here than just displaying them
|
||||
pub fn error_template(errors: RwSignal<Errors>) -> View {
|
||||
view! {
|
||||
<h1>"Errors"</h1>
|
||||
<For
|
||||
// a function that returns the items we're iterating over; a signal is fine
|
||||
each=move || errors.get()
|
||||
// a unique key for each item as a reference
|
||||
key=|(key, _)| key.clone()
|
||||
// renders each item to a view
|
||||
children= move | (_, error)| {
|
||||
let error_string = error.to_string();
|
||||
view! {
|
||||
|
||||
<p>"Error: " {error_string}</p>
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
.into_view()
|
||||
}
|
||||
47
examples/sso_auth_axum/src/fallback.rs
Normal file
47
examples/sso_auth_axum/src/fallback.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
extract::State,
|
||||
response::IntoResponse,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
};
|
||||
use axum::response::Response as AxumResponse;
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
use leptos::*;
|
||||
use crate::error_template::error_template;
|
||||
|
||||
pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse {
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else {
|
||||
leptos::logging::log!("{:?}:{}",res.status(),uri);
|
||||
let handler = leptos_axum::render_app_to_stream(
|
||||
options.to_owned(),
|
||||
|| error_template(create_rw_signal(leptos::Errors::default())
|
||||
)
|
||||
);
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||
let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap();
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// This path is relative to the cargo root
|
||||
match ServeDir::new(root).oneshot(req).await {
|
||||
Ok(res) => Ok(res.map(boxed)),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {}", err),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
162
examples/sso_auth_axum/src/lib.rs
Normal file
162
examples/sso_auth_axum/src/lib.rs
Normal file
@@ -0,0 +1,162 @@
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
pub mod auth;
|
||||
pub mod error_template;
|
||||
pub mod fallback;
|
||||
pub mod sign_in_sign_up;
|
||||
pub mod state;
|
||||
use leptos::{leptos_dom::helpers::TimeoutHandle, *};
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
use sign_in_sign_up::*;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use crate::{
|
||||
state::AppState,
|
||||
auth::{AuthSession,User,SqlRefreshToken}
|
||||
};
|
||||
use oauth2::{
|
||||
reqwest::async_http_client,
|
||||
TokenResponse
|
||||
};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
pub fn pool() -> Result<SqlitePool, ServerFnError> {
|
||||
use_context::<SqlitePool>()
|
||||
.ok_or_else(|| ServerFnError::ServerError("Pool missing.".into()))
|
||||
}
|
||||
|
||||
pub fn auth() -> Result<AuthSession, ServerFnError> {
|
||||
use_context::<AuthSession>()
|
||||
.ok_or_else(|| ServerFnError::ServerError("Auth session missing.".into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Email(RwSignal<Option<String>>);
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ExpiresIn(RwSignal<u64>);
|
||||
#[server]
|
||||
pub async fn refresh_token(email: String) -> Result<u64, ServerFnError> {
|
||||
let pool = pool()?;
|
||||
let oauth_client = expect_context::<AppState>().client;
|
||||
let user = User::get_from_email(&email, &pool)
|
||||
.await
|
||||
.ok_or(ServerFnError::ServerError("User not found".to_string()))?;
|
||||
|
||||
let refresh_secret = sqlx::query_as::<_, SqlRefreshToken>(
|
||||
"SELECT secret FROM google_refresh_tokens WHERE user_id = ?",
|
||||
)
|
||||
.bind(user.id)
|
||||
.fetch_one(&pool)
|
||||
.await?
|
||||
.secret;
|
||||
|
||||
let token_response = oauth_client
|
||||
.exchange_refresh_token(&oauth2::RefreshToken::new(refresh_secret))
|
||||
.request_async(async_http_client)
|
||||
.await?;
|
||||
|
||||
let access_token = token_response.access_token().secret();
|
||||
let expires_in = token_response.expires_in().unwrap().as_secs();
|
||||
let refresh_secret = token_response.refresh_token().unwrap().secret();
|
||||
sqlx::query("DELETE FROM google_tokens WHERE user_id == ?")
|
||||
.bind(user.id)
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
sqlx::query(
|
||||
"INSERT INTO google_tokens (user_id,access_secret,refresh_secret) \
|
||||
VALUES (?,?,?)",
|
||||
)
|
||||
.bind(user.id)
|
||||
.bind(access_token)
|
||||
.bind(refresh_secret)
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
Ok(expires_in)
|
||||
}
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
provide_meta_context();
|
||||
let email = create_rw_signal(None::<String>);
|
||||
let rw_expires_in = create_rw_signal(0);
|
||||
provide_context(Email(email));
|
||||
provide_context(ExpiresIn(rw_expires_in));
|
||||
|
||||
let display_email =
|
||||
move || email.get().unwrap_or(String::from("No email to display"));
|
||||
let refresh_token = create_server_action::<RefreshToken>();
|
||||
|
||||
create_effect(move |handle: Option<Option<TimeoutHandle>>| {
|
||||
// If this effect is called, try to cancel the previous handle.
|
||||
if let Some(prev_handle) = handle.flatten() {
|
||||
prev_handle.clear();
|
||||
};
|
||||
// if expires_in isn't 0, then set a timeout that rerfresh a minute short of the refresh.
|
||||
let expires_in = rw_expires_in.get();
|
||||
if expires_in != 0 && email.get_untracked().is_some() {
|
||||
let handle = set_timeout_with_handle(
|
||||
move || {
|
||||
refresh_token.dispatch(RefreshToken {
|
||||
email: email.get_untracked().unwrap(),
|
||||
})
|
||||
},
|
||||
std::time::Duration::from_secs(
|
||||
// Google tokens last 3599 seconds, so we'll get a refresh token every 14 seconds.
|
||||
expires_in.checked_sub(3545).unwrap_or_default(),
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
Some(handle)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
create_effect(move |_| {
|
||||
if let Some(Ok(expires_in)) = refresh_token.value().get() {
|
||||
rw_expires_in.set(expires_in);
|
||||
}
|
||||
});
|
||||
|
||||
view! {
|
||||
<Stylesheet id="leptos" href="/pkg/sso_auth_axum.css"/>
|
||||
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
||||
<Title text="SSO Auth Axum"/>
|
||||
<Router>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" view=move || {
|
||||
view!{
|
||||
{display_email}
|
||||
<Show when=move||email.get().is_some() fallback=||view!{<SignIn/>}>
|
||||
<LogOut/>
|
||||
</Show>
|
||||
}
|
||||
}/>
|
||||
<Route path="g_auth" view=||view!{<HandleGAuth/>}/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "hydrate")] {
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use leptos::view;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
leptos::mount_to_body(|| {
|
||||
view! { <App/> }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
136
examples/sso_auth_axum/src/main.rs
Normal file
136
examples/sso_auth_axum/src/main.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
// boilerplate to run in different modes
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
response::{IntoResponse},
|
||||
routing::get,
|
||||
extract::{Path, State, RawQuery},
|
||||
http::{Request, header::HeaderMap},
|
||||
body::Body as AxumBody,
|
||||
Router,
|
||||
};
|
||||
use sso_auth_axum::auth::*;
|
||||
use sso_auth_axum::state::AppState;
|
||||
use sso_auth_axum::fallback::file_and_error_handler;
|
||||
use leptos_axum::{generate_route_list, handle_server_fns_with_context, LeptosRoutes};
|
||||
use leptos::{logging::log, view, provide_context, get_configuration};
|
||||
use sqlx::{SqlitePool, sqlite::SqlitePoolOptions};
|
||||
use axum_session::{SessionConfig, SessionLayer, SessionStore,Key, SecurityMode};
|
||||
use axum_session_auth::{AuthSessionLayer, AuthConfig, SessionSqlitePool};
|
||||
|
||||
async fn server_fn_handler(State(app_state): State<AppState>, auth_session: AuthSession, path: Path<String>, headers: HeaderMap, raw_query: RawQuery,
|
||||
request: Request<AxumBody>) -> impl IntoResponse {
|
||||
|
||||
log!("{:?}", path);
|
||||
|
||||
handle_server_fns_with_context(path, headers, raw_query, move || {
|
||||
provide_context(app_state.clone());
|
||||
provide_context(auth_session.clone());
|
||||
provide_context(app_state.pool.clone());
|
||||
}, request).await
|
||||
}
|
||||
|
||||
pub async fn leptos_routes_handler(
|
||||
auth_session: AuthSession,
|
||||
State(app_state): State<AppState>,
|
||||
axum::extract::State(option): axum::extract::State<leptos::LeptosOptions>,
|
||||
request: Request<AxumBody>,
|
||||
) -> axum::response::Response {
|
||||
let handler = leptos_axum::render_app_async_with_context(
|
||||
option.clone(),
|
||||
move || {
|
||||
provide_context(app_state.clone());
|
||||
provide_context(auth_session.clone());
|
||||
provide_context(app_state.pool.clone());
|
||||
},
|
||||
move || view! { <sso_auth_axum::App/> },
|
||||
);
|
||||
|
||||
handler(request).await.into_response()
|
||||
}
|
||||
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
simple_logger::init_with_level(log::Level::Info).expect("couldn't initialize logging");
|
||||
|
||||
let pool = SqlitePoolOptions::new()
|
||||
.connect("sqlite:sso.db")
|
||||
.await
|
||||
.expect("Could not make pool.");
|
||||
|
||||
// Auth section
|
||||
let session_config = SessionConfig::default()
|
||||
.with_table_name("sessions_table")
|
||||
.with_key(Key::generate())
|
||||
.with_database_key(Key::generate())
|
||||
.with_security_mode(SecurityMode::PerSession);
|
||||
|
||||
let auth_config = AuthConfig::<i64>::default();
|
||||
let session_store = SessionStore::<SessionSqlitePool>::new(Some(pool.clone().into()), session_config).await.unwrap();
|
||||
|
||||
sqlx::migrate!()
|
||||
.run(&pool)
|
||||
.await
|
||||
.expect("could not run SQLx migrations");
|
||||
|
||||
|
||||
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
let leptos_options = conf.leptos_options;
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = generate_route_list(sso_auth_axum::App);
|
||||
|
||||
// We create our client using provided environment variables.
|
||||
let client = oauth2::basic::BasicClient::new(
|
||||
oauth2::ClientId::new(std::env::var("G_AUTH_CLIENT_ID").expect("G_AUTH_CLIENT Env var to be set.")),
|
||||
Some(oauth2::ClientSecret::new(std::env::var("G_AUTH_SECRET").expect("G_AUTH_SECRET Env var to be set"))),
|
||||
oauth2::AuthUrl::new(
|
||||
"https://accounts.google.com/o/oauth2/v2/auth".to_string(),
|
||||
)
|
||||
.unwrap(),
|
||||
Some(
|
||||
oauth2::TokenUrl::new("https://oauth2.googleapis.com/token".to_string())
|
||||
.unwrap(),
|
||||
),
|
||||
)
|
||||
.set_redirect_uri(oauth2::RedirectUrl::new(std::env::var("REDIRECT_URL").expect("REDIRECT_URL Env var to be set")).unwrap());
|
||||
|
||||
|
||||
let app_state = AppState{
|
||||
leptos_options,
|
||||
pool: pool.clone(),
|
||||
client,
|
||||
};
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/api/*fn_name", get(server_fn_handler).post(server_fn_handler))
|
||||
.leptos_routes_with_handler(routes, get(leptos_routes_handler) )
|
||||
.fallback(file_and_error_handler)
|
||||
.layer(AuthSessionLayer::<User, i64, SessionSqlitePool, SqlitePool>::new(Some(pool.clone()))
|
||||
.with_config(auth_config))
|
||||
.layer(SessionLayer::new(session_store))
|
||||
.with_state(app_state);
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
log!("listening on http://{}", &addr);
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// client-only stuff for Trunk
|
||||
else {
|
||||
pub fn main() {
|
||||
// This example cannot be built as a trunk standalone CSR-only app.
|
||||
// Only the server may directly connect to the database.
|
||||
}
|
||||
}
|
||||
}
|
||||
208
examples/sso_auth_axum/src/sign_in_sign_up.rs
Normal file
208
examples/sso_auth_axum/src/sign_in_sign_up.rs
Normal file
@@ -0,0 +1,208 @@
|
||||
use super::*;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature="ssr")]{
|
||||
use oauth2::{
|
||||
AuthorizationCode,
|
||||
TokenResponse,
|
||||
reqwest::async_http_client,
|
||||
CsrfToken,
|
||||
Scope,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use crate::{
|
||||
auth::{User,SqlCsrfToken},
|
||||
state::AppState
|
||||
};
|
||||
}
|
||||
}
|
||||
#[server]
|
||||
pub async fn google_sso() -> Result<String, ServerFnError> {
|
||||
let oauth_client = expect_context::<AppState>().client;
|
||||
let pool = pool()?;
|
||||
|
||||
// We get the authorization URL and CSRF_TOKEN
|
||||
let (authorize_url, csrf_token) = oauth_client
|
||||
.authorize_url(CsrfToken::new_random)
|
||||
.add_scope(Scope::new("openid".to_string()))
|
||||
.add_scope(Scope::new("email".to_string()))
|
||||
// required for google auth refresh token to be part of the response.
|
||||
.add_extra_param("access_type", "offline")
|
||||
.add_extra_param("prompt", "consent")
|
||||
.url();
|
||||
let url = authorize_url.to_string();
|
||||
leptos::logging::log!("{url:?}");
|
||||
// Store the CSRF_TOKEN in our sqlite db.
|
||||
sqlx::query("INSERT INTO csrf_tokens (csrf_token) VALUES (?)")
|
||||
.bind(csrf_token.secret())
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map(|_| ())?;
|
||||
|
||||
// Send the url to the client.
|
||||
Ok(url)
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn SignIn() -> impl IntoView {
|
||||
let g_auth = Action::<GoogleSso, _>::server();
|
||||
|
||||
create_effect(move |_| {
|
||||
if let Some(Ok(redirect)) = g_auth.value().get() {
|
||||
window().location().set_href(&redirect).unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
view! {
|
||||
<div style="
|
||||
display:flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
">
|
||||
<div> {"Sign Up Sign In"} </div>
|
||||
<button style="display:flex;" on:click=move|_| g_auth.dispatch(GoogleSso{})>
|
||||
<svg style="width:2rem;" version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" xmlns:xlink="http://www.w3.org/1999/xlink" style="display: block;">
|
||||
<path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"></path>
|
||||
<path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"></path>
|
||||
<path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"></path>
|
||||
<path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"></path>
|
||||
<path fill="none" d="M0 0h48v48H0z"></path>
|
||||
</svg>
|
||||
<span style="margin-left:0.5rem;">"Sign in with Google"</span>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn handle_g_auth_redirect(
|
||||
provided_csrf: String,
|
||||
code: String,
|
||||
) -> Result<(String, u64), ServerFnError> {
|
||||
let oauth_client = expect_context::<AppState>().client;
|
||||
let pool = pool()?;
|
||||
let auth_session = auth()?;
|
||||
// If there's no match we'll return an error.
|
||||
let _ = sqlx::query_as::<_, SqlCsrfToken>(
|
||||
"SELECT csrf_token FROM csrf_tokens WHERE csrf_token = ?",
|
||||
)
|
||||
.bind(provided_csrf)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
ServerFnError::ServerError(format!("CSRF_TOKEN error : {err:?}"))
|
||||
})?;
|
||||
|
||||
let token_response = oauth_client
|
||||
.exchange_code(AuthorizationCode::new(code.clone()))
|
||||
.request_async(async_http_client)
|
||||
.await?;
|
||||
leptos::logging::log!("{:?}", &token_response);
|
||||
let access_token = token_response.access_token().secret();
|
||||
let expires_in = token_response.expires_in().unwrap().as_secs();
|
||||
let refresh_secret = token_response.refresh_token().unwrap().secret();
|
||||
let user_info_url = "https://www.googleapis.com/oauth2/v3/userinfo";
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.get(user_info_url)
|
||||
.bearer_auth(access_token)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let email = if response.status().is_success() {
|
||||
let response_json: Value = response.json().await?;
|
||||
leptos::logging::log!("{response_json:?}");
|
||||
response_json["email"]
|
||||
.as_str()
|
||||
.expect("email to parse to string")
|
||||
.to_string()
|
||||
} else {
|
||||
return Err(ServerFnError::ServerError(format!(
|
||||
"Response from google has status of {}",
|
||||
response.status()
|
||||
)));
|
||||
};
|
||||
|
||||
let user = if let Some(user) = User::get_from_email(&email, &pool).await {
|
||||
user
|
||||
} else {
|
||||
sqlx::query("INSERT INTO users (email) VALUES (?)")
|
||||
.bind(&email)
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
User::get_from_email(&email, &pool).await.unwrap()
|
||||
};
|
||||
|
||||
auth_session.login_user(user.id);
|
||||
|
||||
sqlx::query("DELETE FROM google_tokens WHERE user_id == ?")
|
||||
.bind(user.id)
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO google_tokens (user_id,access_secret,refresh_secret) \
|
||||
VALUES (?,?,?)",
|
||||
)
|
||||
.bind(user.id)
|
||||
.bind(access_token)
|
||||
.bind(refresh_secret)
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
|
||||
Ok((user.email, expires_in as u64))
|
||||
}
|
||||
|
||||
#[derive(Params, Debug, PartialEq, Clone)]
|
||||
pub struct OAuthParams {
|
||||
pub code: Option<String>,
|
||||
pub state: Option<String>,
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn HandleGAuth() -> impl IntoView {
|
||||
let handle_g_auth_redirect = Action::<HandleGAuthRedirect, _>::server();
|
||||
|
||||
let query = use_query::<OAuthParams>();
|
||||
let navigate = leptos_router::use_navigate();
|
||||
let rw_email = expect_context::<Email>().0;
|
||||
let rw_expires_in = expect_context::<ExpiresIn>().0;
|
||||
create_effect(move |_| {
|
||||
if let Some(Ok((email, expires_in))) =
|
||||
handle_g_auth_redirect.value().get()
|
||||
{
|
||||
rw_email.set(Some(email));
|
||||
rw_expires_in.set(expires_in);
|
||||
navigate("/", NavigateOptions::default());
|
||||
}
|
||||
});
|
||||
|
||||
create_effect(move |_| {
|
||||
if let Ok(OAuthParams { code, state }) = query.get_untracked() {
|
||||
handle_g_auth_redirect.dispatch(HandleGAuthRedirect {
|
||||
provided_csrf: state.unwrap(),
|
||||
code: code.unwrap(),
|
||||
});
|
||||
} else {
|
||||
leptos::logging::log!("error parsing oauth params");
|
||||
}
|
||||
});
|
||||
view! {}
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn logout() -> Result<(), ServerFnError> {
|
||||
let auth = auth()?;
|
||||
auth.logout_user();
|
||||
leptos_axum::redirect("/");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn LogOut() -> impl IntoView {
|
||||
let log_out = create_server_action::<Logout>();
|
||||
view! {
|
||||
<button on:click=move|_|log_out.dispatch(Logout{})>{"log out"}</button>
|
||||
}
|
||||
}
|
||||
18
examples/sso_auth_axum/src/state.rs
Normal file
18
examples/sso_auth_axum/src/state.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use leptos::LeptosOptions;
|
||||
use sqlx::SqlitePool;
|
||||
use axum::extract::FromRef;
|
||||
|
||||
/// This takes advantage of Axum's SubStates feature by deriving FromRef. This is the only way to have more than one
|
||||
/// item in Axum's State. Leptos requires you to have leptosOptions in your State struct for the leptos route handlers
|
||||
#[derive(FromRef, Debug, Clone)]
|
||||
pub struct AppState{
|
||||
pub leptos_options: LeptosOptions,
|
||||
pub pool: SqlitePool,
|
||||
pub client:oauth2::basic::BasicClient,
|
||||
}
|
||||
}
|
||||
}
|
||||
0
examples/sso_auth_axum/sso.db
Normal file
0
examples/sso_auth_axum/sso.db
Normal file
7
examples/sso_auth_axum/style.css
Normal file
7
examples/sso_auth_axum/style.css
Normal file
@@ -0,0 +1,7 @@
|
||||
.pending {
|
||||
color: purple;
|
||||
}
|
||||
|
||||
a {
|
||||
color: black;
|
||||
}
|
||||
2
examples/ssr_modes/rust-toolchain.toml
Normal file
2
examples/ssr_modes/rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
2
examples/ssr_modes_axum/rust-toolchain.toml
Normal file
2
examples/ssr_modes_axum/rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
@@ -9,11 +9,6 @@ cfg_if! {
|
||||
use crate::app::*;
|
||||
use leptos_actix::{generate_route_list, LeptosRoutes};
|
||||
|
||||
#[get("/style.css")]
|
||||
async fn css() -> impl Responder {
|
||||
actix_files::NamedFile::open_async("./style/output.css").await
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
|
||||
@@ -30,7 +25,6 @@ cfg_if! {
|
||||
let site_root = &leptos_options.site_root;
|
||||
let routes = &routes;
|
||||
App::new()
|
||||
.service(css)
|
||||
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), || view! { <App/> })
|
||||
.service(Files::new("/", site_root))
|
||||
.wrap(middleware::Compress::default())
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
|
||||
2
examples/tailwind_csr/rust-toolchain.toml
Normal file
2
examples/tailwind_csr/rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
2
examples/todo_app_sqlite/rust-toolchain.toml
Normal file
2
examples/todo_app_sqlite/rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
@@ -22,6 +22,7 @@ cfg_if! {
|
||||
}
|
||||
}
|
||||
|
||||
/// Server functions can be given doc comments.
|
||||
#[server(GetTodos, "/api")]
|
||||
pub async fn get_todos() -> Result<Vec<Todo>, ServerFnError> {
|
||||
// this is just an example of how to access server context injected in the handlers
|
||||
|
||||
2
examples/todo_app_sqlite_axum/rust-toolchain.toml
Normal file
2
examples/todo_app_sqlite_axum/rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
2
examples/todo_app_sqlite_csr/rust-toolchain.toml
Normal file
2
examples/todo_app_sqlite_csr/rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
2
examples/todo_app_sqlite_viz/rust-toolchain.toml
Normal file
2
examples/todo_app_sqlite_viz/rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
2
examples/todomvc/rust-toolchain.toml
Normal file
2
examples/todomvc/rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
@@ -7,7 +7,7 @@
|
||||
//! To run in this environment, you need to disable the default feature set and enable
|
||||
//! the `wasm` feature on `leptos_axum` in your `Cargo.toml`.
|
||||
//! ```toml
|
||||
//! leptos_axum = { version = "0.5.4", default-features = false, features = ["wasm"] }
|
||||
//! leptos_axum = { version = "", default-features = false, features = ["wasm"] }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Features
|
||||
@@ -457,7 +457,7 @@ pub type PinnedHtmlStream =
|
||||
/// - [ResponseOptions]
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "info", fields(error), skip_all)]
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
pub fn render_app_to_stream<IV>(
|
||||
options: LeptosOptions,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
@@ -483,7 +483,7 @@ where
|
||||
/// The difference between calling this and `render_app_to_stream_with_context()` is that this
|
||||
/// one respects the `SsrMode` on each Route and thus requires `Vec<RouteListing>` for route checking.
|
||||
/// This is useful if you are using `.leptos_routes_with_handler()`
|
||||
#[tracing::instrument(level = "info", fields(error), skip_all)]
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
pub fn render_route<IV>(
|
||||
options: LeptosOptions,
|
||||
paths: Vec<RouteListing>,
|
||||
@@ -558,7 +558,7 @@ where
|
||||
/// - [ResponseOptions]
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "info", fields(error), skip_all)]
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
pub fn render_app_to_stream_in_order<IV>(
|
||||
options: LeptosOptions,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
@@ -604,7 +604,7 @@ where
|
||||
/// - [ResponseOptions]
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "info", fields(error), skip_all)]
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
pub fn render_app_to_stream_with_context<IV>(
|
||||
options: LeptosOptions,
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
@@ -636,7 +636,7 @@ where
|
||||
/// The difference between calling this and `render_app_to_stream_with_context()` is that this
|
||||
/// one respects the `SsrMode` on each Route, and thus requires `Vec<RouteListing>` for route checking.
|
||||
/// This is useful if you are using `.leptos_routes_with_handler()`.
|
||||
#[tracing::instrument(level = "info", fields(error), skip_all)]
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
pub fn render_route_with_context<IV>(
|
||||
options: LeptosOptions,
|
||||
paths: Vec<RouteListing>,
|
||||
@@ -724,7 +724,7 @@ where
|
||||
/// - [ResponseOptions]
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "info", fields(error), skip_all)]
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
pub fn render_app_to_stream_with_context_and_replace_blocks<IV>(
|
||||
options: LeptosOptions,
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
@@ -787,7 +787,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "info", fields(error), skip_all)]
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
async fn generate_response(
|
||||
res_options: ResponseOptions,
|
||||
rx: Receiver<String>,
|
||||
@@ -829,7 +829,7 @@ async fn generate_response(
|
||||
}
|
||||
res
|
||||
}
|
||||
#[tracing::instrument(level = "info", fields(error), skip_all)]
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
async fn forward_stream(
|
||||
options: &LeptosOptions,
|
||||
res_options2: ResponseOptions,
|
||||
@@ -890,7 +890,7 @@ async fn forward_stream(
|
||||
/// - [ResponseOptions]
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "info", fields(error), skip_all)]
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
pub fn render_app_to_stream_in_order_with_context<IV>(
|
||||
options: LeptosOptions,
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
@@ -1026,7 +1026,7 @@ fn provide_contexts(
|
||||
/// - [ResponseOptions]
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "info", fields(error), skip_all)]
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
pub fn render_app_async<IV>(
|
||||
options: LeptosOptions,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
@@ -1068,7 +1068,7 @@ where
|
||||
/// - [ResponseOptions]
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "info", fields(error), skip_all)]
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
pub fn render_app_async_stream_with_context<IV>(
|
||||
options: LeptosOptions,
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
@@ -1204,7 +1204,7 @@ where
|
||||
/// - [ResponseOptions]
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "info", fields(error), skip_all)]
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
pub fn render_app_async_with_context<IV>(
|
||||
options: LeptosOptions,
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
@@ -1677,7 +1677,7 @@ where
|
||||
LeptosOptions: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
#[tracing::instrument(level = "info", fields(error), skip_all)]
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
fn leptos_routes<IV>(
|
||||
self,
|
||||
options: &S,
|
||||
|
||||
@@ -45,7 +45,7 @@ use leptos_reactive::{
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "info", skip_all)
|
||||
tracing::instrument(level = "trace", skip_all)
|
||||
)]
|
||||
#[component]
|
||||
pub fn AnimatedShow(
|
||||
|
||||
@@ -52,7 +52,7 @@ use std::hash::Hash;
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "info", skip_all)
|
||||
tracing::instrument(level = "trace", skip_all)
|
||||
)]
|
||||
#[component(transparent)]
|
||||
pub fn For<IF, I, T, EF, N, KF, K>(
|
||||
|
||||
@@ -57,8 +57,8 @@
|
||||
//! [`todo_app_sqlite_axum`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite_axum), and
|
||||
//! [`todo_app_sqlite_viz`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite_viz)
|
||||
//! show how to build a full-stack app using server functions and database connections.
|
||||
//! - [`tailwind`](https://github.com/leptos-rs/leptos/tree/main/examples/tailwind) shows how to integrate
|
||||
//! TailwindCSS with `cargo-leptos`.
|
||||
//! - [`tailwind`](https://github.com/leptos-rs/leptos/tree/main/examples/tailwind_csr) shows how to integrate
|
||||
//! TailwindCSS with `trunk` for CSR.
|
||||
//!
|
||||
//! Details on how to run each example can be found in its README.
|
||||
//!
|
||||
|
||||
@@ -11,7 +11,7 @@ use leptos_macro::component;
|
||||
/// Setting `use_shadow` to `true` places the element in a shadow root to isolate styles.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "info", skip_all)
|
||||
tracing::instrument(level = "trace", skip_all)
|
||||
)]
|
||||
#[component]
|
||||
pub fn Portal(
|
||||
|
||||
@@ -28,7 +28,7 @@ use leptos_reactive::{create_memo, signal_prelude::*};
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "info", skip_all)
|
||||
tracing::instrument(level = "trace", skip_all)
|
||||
)]
|
||||
#[component]
|
||||
pub fn Show<W>(
|
||||
|
||||
@@ -57,7 +57,7 @@ use std::rc::Rc;
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "info", skip_all)
|
||||
tracing::instrument(level = "trace", skip_all)
|
||||
)]
|
||||
#[component]
|
||||
pub fn Suspense<V>(
|
||||
|
||||
@@ -64,7 +64,7 @@ use std::{
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "info", skip_all)
|
||||
tracing::instrument(level = "trace", skip_all)
|
||||
)]
|
||||
#[component(transparent)]
|
||||
pub fn Transition(
|
||||
@@ -130,7 +130,8 @@ pub fn Transition(
|
||||
}
|
||||
if is_first_run(first_run, &suspense_context) {
|
||||
let has_local_only = suspense_context.has_local_only()
|
||||
|| cfg!(feature = "csr");
|
||||
|| cfg!(feature = "csr")
|
||||
|| !HydrationCtx::is_hydrating();
|
||||
if (!has_local_only || child_runs.get() > 0)
|
||||
&& !cfg!(feature = "csr")
|
||||
{
|
||||
|
||||
@@ -16,7 +16,7 @@ futures = "0.3"
|
||||
getrandom = { version = "0.2", optional = true }
|
||||
html-escape = "0.2"
|
||||
indexmap = "2"
|
||||
itertools = "0.10"
|
||||
itertools = "0.12"
|
||||
js-sys = "0.3"
|
||||
leptos_reactive = { workspace = true }
|
||||
server_fn = { workspace = true }
|
||||
|
||||
@@ -158,7 +158,7 @@ impl From<ComponentRepr> for View {
|
||||
}
|
||||
|
||||
impl IntoView for ComponentRepr {
|
||||
#[cfg_attr(any(debug_assertions, feature = "ssr"), instrument(level = "info", name = "<Component />", skip_all, fields(name = %self.name)))]
|
||||
#[cfg_attr(any(debug_assertions, feature = "ssr"), instrument(level = "trace", name = "<Component />", skip_all, fields(name = %self.name)))]
|
||||
fn into_view(self) -> View {
|
||||
self.into()
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ where
|
||||
{
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "info", name = "<DynChild />", skip_all)
|
||||
instrument(level = "trace", name = "<DynChild />", skip_all)
|
||||
)]
|
||||
#[inline]
|
||||
fn into_view(self) -> View {
|
||||
|
||||
@@ -382,7 +382,7 @@ where
|
||||
{
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "info", name = "<Each />", skip_all)
|
||||
instrument(level = "trace", name = "<Each />", skip_all)
|
||||
)]
|
||||
fn into_view(self) -> crate::View {
|
||||
let Self {
|
||||
|
||||
@@ -15,7 +15,7 @@ where
|
||||
{
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "info", skip_all,)
|
||||
instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
fn into_fragment(self) -> Fragment {
|
||||
self.into_iter().map(|v| v.into_view()).collect()
|
||||
@@ -110,7 +110,7 @@ impl Fragment {
|
||||
}
|
||||
|
||||
impl IntoView for Fragment {
|
||||
#[cfg_attr(debug_assertions, instrument(level = "info", name = "</>", skip_all, fields(children = self.nodes.len())))]
|
||||
#[cfg_attr(debug_assertions, instrument(level = "trace", name = "</>", skip_all, fields(children = self.nodes.len())))]
|
||||
fn into_view(self) -> View {
|
||||
self.into()
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ pub struct Unit;
|
||||
impl IntoView for Unit {
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "info", name = "<() />", skip_all)
|
||||
instrument(level = "trace", name = "<() />", skip_all)
|
||||
)]
|
||||
fn into_view(self) -> crate::View {
|
||||
let component = UnitRepr::default();
|
||||
|
||||
@@ -53,7 +53,7 @@ mod hydrate_only {
|
||||
}
|
||||
});
|
||||
|
||||
pub static IS_HYDRATING: Cell<bool> = Cell::new(true);
|
||||
pub static IS_HYDRATING: Cell<bool> = const { Cell::new(true) };
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
@@ -133,7 +133,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
thread_local!(static ID: RefCell<HydrationKey> = RefCell::new(HydrationKey { outlet: 0, fragment: 0, error: 0, id: 0 }));
|
||||
thread_local!(static ID: RefCell<HydrationKey> = const {RefCell::new(HydrationKey { outlet: 0, fragment: 0, error: 0, id: 0 })});
|
||||
|
||||
/// Control and utility methods for hydration.
|
||||
pub struct HydrationCtx;
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
#![forbid(unsafe_code)]
|
||||
#![cfg_attr(feature = "nightly", feature(fn_traits))]
|
||||
#![cfg_attr(feature = "nightly", feature(unboxed_closures))]
|
||||
// to prevent warnings from popping up when a nightly feature is stabilized
|
||||
#![allow(stable_features)]
|
||||
|
||||
//! The DOM implementation for `leptos`.
|
||||
|
||||
@@ -99,7 +101,7 @@ pub trait Mountable {
|
||||
impl IntoView for () {
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "info", name = "<() />", skip_all)
|
||||
instrument(level = "trace", name = "<() />", skip_all)
|
||||
)]
|
||||
fn into_view(self) -> View {
|
||||
Unit.into_view()
|
||||
@@ -112,7 +114,7 @@ where
|
||||
{
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "info", name = "Option<T>", skip_all)
|
||||
instrument(level = "trace", name = "Option<T>", skip_all)
|
||||
)]
|
||||
fn into_view(self) -> View {
|
||||
if let Some(t) = self {
|
||||
@@ -130,7 +132,7 @@ where
|
||||
{
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "info", name = "Fn() -> impl IntoView", skip_all)
|
||||
instrument(level = "trace", name = "Fn() -> impl IntoView", skip_all)
|
||||
)]
|
||||
#[track_caller]
|
||||
fn into_view(self) -> View {
|
||||
@@ -244,7 +246,7 @@ pub trait CollectView {
|
||||
impl<I: IntoIterator<Item = T>, T: IntoView> CollectView for I {
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "info", name = "#text", skip_all)
|
||||
instrument(level = "trace", name = "#text", skip_all)
|
||||
)]
|
||||
fn collect_view(self) -> View {
|
||||
self.into_iter()
|
||||
@@ -379,7 +381,7 @@ impl Element {
|
||||
}
|
||||
|
||||
impl IntoView for Element {
|
||||
#[cfg_attr(debug_assertions, instrument(level = "info", name = "<Element />", skip_all, fields(tag = %self.name)))]
|
||||
#[cfg_attr(debug_assertions, instrument(level = "trace", name = "<Element />", skip_all, fields(tag = %self.name)))]
|
||||
fn into_view(self) -> View {
|
||||
View::Element(self)
|
||||
}
|
||||
@@ -499,7 +501,7 @@ impl fmt::Debug for Text {
|
||||
}
|
||||
|
||||
impl IntoView for Text {
|
||||
#[cfg_attr(debug_assertions, instrument(level = "info", name = "#text", skip_all, fields(content = %self.content)))]
|
||||
#[cfg_attr(debug_assertions, instrument(level = "trace", name = "#text", skip_all, fields(content = %self.content)))]
|
||||
fn into_view(self) -> View {
|
||||
View::Text(self)
|
||||
}
|
||||
@@ -564,7 +566,7 @@ impl Default for View {
|
||||
}
|
||||
|
||||
impl IntoView for View {
|
||||
#[cfg_attr(debug_assertions, instrument(level = "info", name = "Node", skip_all, fields(kind = self.kind_name())))]
|
||||
#[cfg_attr(debug_assertions, instrument(level = "trace", name = "Node", skip_all, fields(kind = self.kind_name())))]
|
||||
fn into_view(self) -> View {
|
||||
self
|
||||
}
|
||||
@@ -579,7 +581,7 @@ impl IntoView for &View {
|
||||
impl<const N: usize> IntoView for [View; N] {
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "info", name = "[Node; N]", skip_all)
|
||||
instrument(level = "trace", name = "[Node; N]", skip_all)
|
||||
)]
|
||||
fn into_view(self) -> View {
|
||||
Fragment::new(self.into_iter().collect()).into_view()
|
||||
@@ -1133,7 +1135,7 @@ api_planning! {
|
||||
impl IntoView for String {
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "info", name = "#text", skip_all)
|
||||
instrument(level = "trace", name = "#text", skip_all)
|
||||
)]
|
||||
#[inline(always)]
|
||||
fn into_view(self) -> View {
|
||||
@@ -1144,7 +1146,7 @@ impl IntoView for String {
|
||||
impl IntoView for &'static str {
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "info", name = "#text", skip_all)
|
||||
instrument(level = "trace", name = "#text", skip_all)
|
||||
)]
|
||||
#[inline(always)]
|
||||
fn into_view(self) -> View {
|
||||
@@ -1155,7 +1157,7 @@ impl IntoView for &'static str {
|
||||
impl IntoView for Oco<'static, str> {
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "info", name = "#text", skip_all)
|
||||
instrument(level = "trace", name = "#text", skip_all)
|
||||
)]
|
||||
#[inline(always)]
|
||||
fn into_view(self) -> View {
|
||||
@@ -1169,7 +1171,7 @@ where
|
||||
{
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "info", name = "#text", skip_all)
|
||||
instrument(level = "trace", name = "#text", skip_all)
|
||||
)]
|
||||
fn into_view(self) -> View {
|
||||
self.into_iter()
|
||||
@@ -1182,7 +1184,7 @@ where
|
||||
impl IntoView for core::fmt::Arguments<'_> {
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "info", name = "#text", skip_all)
|
||||
instrument(level = "trace", name = "#text", skip_all)
|
||||
)]
|
||||
fn into_view(self) -> View {
|
||||
match self.as_str() {
|
||||
|
||||
@@ -138,15 +138,6 @@ macro_rules! impl_into_attr_boxed {
|
||||
};
|
||||
}
|
||||
|
||||
impl IntoAttribute for Option<Attribute> {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self) -> Attribute {
|
||||
self.unwrap_or(Attribute::Option(None))
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl IntoAttribute for String {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self) -> Attribute {
|
||||
@@ -201,46 +192,10 @@ impl IntoAttribute for bool {
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl IntoAttribute for Option<String> {
|
||||
impl<T: IntoAttribute> IntoAttribute for Option<T> {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self) -> Attribute {
|
||||
Attribute::Option(self.map(Oco::Owned))
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl IntoAttribute for Option<&'static str> {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self) -> Attribute {
|
||||
Attribute::Option(self.map(Oco::Borrowed))
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl IntoAttribute for Option<Rc<str>> {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self) -> Attribute {
|
||||
Attribute::Option(self.map(Oco::Counted))
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl IntoAttribute for Option<Cow<'static, str>> {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self) -> Attribute {
|
||||
Attribute::Option(self.map(Oco::from))
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl IntoAttribute for Option<Oco<'static, str>> {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self) -> Attribute {
|
||||
Attribute::Option(self)
|
||||
self.map_or(Attribute::Option(None), IntoAttribute::into_attribute)
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
@@ -310,17 +265,6 @@ macro_rules! attr_type {
|
||||
self.into_attribute()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoAttribute for Option<$attr_type> {
|
||||
fn into_attribute(self) -> Attribute {
|
||||
Attribute::Option(self.map(|n| n.to_string().into()))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn into_attribute_boxed(self: Box<Self>) -> Attribute {
|
||||
self.into_attribute()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -341,24 +285,6 @@ macro_rules! attr_signal_type {
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! attr_signal_type_optional {
|
||||
($signal_type:ty) => {
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
impl<T> IntoAttribute for $signal_type
|
||||
where
|
||||
T: Clone,
|
||||
Option<T>: IntoAttribute,
|
||||
{
|
||||
fn into_attribute(self) -> Attribute {
|
||||
let modified_fn = Rc::new(move || self.get().into_attribute());
|
||||
Attribute::Fn(modified_fn)
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
attr_type!(&String);
|
||||
attr_type!(usize);
|
||||
attr_type!(u8);
|
||||
@@ -381,7 +307,7 @@ attr_signal_type!(RwSignal<T>);
|
||||
attr_signal_type!(Memo<T>);
|
||||
attr_signal_type!(Signal<T>);
|
||||
attr_signal_type!(MaybeSignal<T>);
|
||||
attr_signal_type_optional!(MaybeProp<T>);
|
||||
attr_signal_type!(MaybeProp<T>);
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
#[doc(hidden)]
|
||||
|
||||
@@ -117,13 +117,13 @@ fn match_primitive() {
|
||||
assert_eq!(prop, r#"{"name": "test", "value": -1}"#);
|
||||
|
||||
// f64
|
||||
let test = 3.14;
|
||||
let test = 3.25;
|
||||
let prop = (&&Match {
|
||||
name: stringify! {test},
|
||||
value: std::cell::Cell::new(Some(&test)),
|
||||
})
|
||||
.spez();
|
||||
assert_eq!(prop, r#"{"name": "test", "value": 3.14}"#);
|
||||
assert_eq!(prop, r#"{"name": "test", "value": 3.25}"#);
|
||||
|
||||
// bool
|
||||
let test = true;
|
||||
|
||||
@@ -69,16 +69,6 @@ impl IntoAttribute for Nonce {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoAttribute for Option<Nonce> {
|
||||
fn into_attribute(self) -> Attribute {
|
||||
Attribute::Option(self.map(|n| n.0.into()))
|
||||
}
|
||||
|
||||
fn into_attribute_boxed(self: Box<Self>) -> Attribute {
|
||||
Attribute::Option(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.
|
||||
|
||||
@@ -28,7 +28,7 @@ type PinnedFuture<T> = Pin<Box<dyn Future<Output = T>>>;
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "info", skip_all,)
|
||||
instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
pub fn render_to_string<F, N>(f: F) -> Oco<'static, str>
|
||||
where
|
||||
@@ -59,7 +59,7 @@ where
|
||||
/// read under that `<Suspense/>` resolve.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "info", skip_all,)
|
||||
instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
pub fn render_to_stream(
|
||||
view: impl FnOnce() -> View + 'static,
|
||||
@@ -83,7 +83,7 @@ pub fn render_to_stream(
|
||||
/// read under that `<Suspense/>` resolve.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "info", skip_all,)
|
||||
instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
pub fn render_to_stream_with_prefix(
|
||||
view: impl FnOnce() -> View + 'static,
|
||||
@@ -112,7 +112,7 @@ pub fn render_to_stream_with_prefix(
|
||||
/// read under that `<Suspense/>` resolve.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "info", skip_all,)
|
||||
instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
pub fn render_to_stream_with_prefix_undisposed(
|
||||
view: impl FnOnce() -> View + 'static,
|
||||
@@ -138,7 +138,7 @@ pub fn render_to_stream_with_prefix_undisposed(
|
||||
/// read under that `<Suspense/>` resolve.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "info", skip_all,)
|
||||
instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
pub fn render_to_stream_with_prefix_undisposed_with_context(
|
||||
view: impl FnOnce() -> View + 'static,
|
||||
@@ -175,7 +175,7 @@ pub fn render_to_stream_with_prefix_undisposed_with_context(
|
||||
/// read under that `<Suspense/>` resolve.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "info", skip_all,)
|
||||
instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
pub fn render_to_stream_with_prefix_undisposed_with_context_and_block_replacement(
|
||||
view: impl FnOnce() -> View + 'static,
|
||||
@@ -365,7 +365,7 @@ impl View {
|
||||
/// Consumes the node and renders it into an HTML string.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "info", skip_all,)
|
||||
instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
pub fn render_to_string(self) -> Oco<'static, str> {
|
||||
#[cfg(all(feature = "web", feature = "ssr"))]
|
||||
|
||||
@@ -17,7 +17,7 @@ use std::collections::VecDeque;
|
||||
|
||||
/// Renders a view to HTML, waiting to return until all `async` [Resource](leptos_reactive::Resource)s
|
||||
/// loaded in `<Suspense/>` elements have finished loading.
|
||||
#[tracing::instrument(level = "info", skip_all)]
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
pub async fn render_to_string_async(
|
||||
view: impl FnOnce() -> View + 'static,
|
||||
) -> String {
|
||||
@@ -40,7 +40,7 @@ pub async fn render_to_string_async(
|
||||
/// in order:
|
||||
/// 1. HTML from the `view` in order, pausing to wait for each `<Suspense/>`
|
||||
/// 2. any serialized [Resource](leptos_reactive::Resource)s
|
||||
#[tracing::instrument(level = "info", skip_all)]
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
pub fn render_to_stream_in_order(
|
||||
view: impl FnOnce() -> View + 'static,
|
||||
) -> impl Stream<Item = String> {
|
||||
|
||||
@@ -15,7 +15,7 @@ proc-macro = true
|
||||
attribute-derive = { version = "0.8", features = ["syn-full"] }
|
||||
cfg-if = "1"
|
||||
html-escape = "0.2"
|
||||
itertools = "0.11"
|
||||
itertools = "0.12"
|
||||
prettyplease = "0.2.4"
|
||||
proc-macro-error = { version = "1", default-features = false }
|
||||
proc-macro2 = "1"
|
||||
|
||||
@@ -38,7 +38,7 @@ pub fn TestComponent(
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn TestMutCallback<'a, F>(mut callback: F, value: &'a str) -> impl IntoView
|
||||
fn TestMutCallback<F>(mut callback: F, value: &'static str) -> impl IntoView
|
||||
where
|
||||
F: FnMut(u32) + 'static,
|
||||
{
|
||||
|
||||
@@ -190,7 +190,7 @@ impl ToTokens for Model {
|
||||
#[allow(clippy::let_with_type_underscore)]
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature="ssr"),
|
||||
::leptos::leptos_dom::tracing::instrument(level = "info", name = #trace_name, skip_all)
|
||||
::leptos::leptos_dom::tracing::instrument(level = "trace", name = #trace_name, skip_all)
|
||||
)]
|
||||
},
|
||||
quote! {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#![cfg_attr(feature = "nightly", feature(proc_macro_span))]
|
||||
#![forbid(unsafe_code)]
|
||||
// to prevent warnings from popping up when a nightly feature is stabilized
|
||||
#![allow(stable_features)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate proc_macro_error;
|
||||
@@ -74,7 +76,8 @@ mod slot;
|
||||
/// # runtime.dispose();
|
||||
/// ```
|
||||
///
|
||||
/// 3. Components (functions annotated with `#[component]`) can be inserted as camel-cased tags
|
||||
/// 3. Components (functions annotated with `#[component]`) can be inserted as camel-cased tags. (Generics
|
||||
/// on components are specified as `<Component<T>/>`, not the turbofish `<Component::<T>/>`.)
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
/// # let runtime = create_runtime();
|
||||
@@ -600,10 +603,12 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
false
|
||||
};
|
||||
|
||||
let mut dummy = syn::parse::<DummyModel>(s.clone());
|
||||
let Ok(mut dummy) = syn::parse::<DummyModel>(s.clone()) else {
|
||||
return s;
|
||||
};
|
||||
let parse_result = syn::parse::<component::Model>(s);
|
||||
|
||||
if let (Ok(ref mut unexpanded), Ok(model)) = (&mut dummy, parse_result) {
|
||||
if let (ref mut unexpanded, Ok(model)) = (&mut dummy, parse_result) {
|
||||
let expanded = model.is_transparent(is_transparent).into_token_stream();
|
||||
unexpanded.sig.ident =
|
||||
unmodified_fn_name_from_fn_name(&unexpanded.sig.ident);
|
||||
@@ -613,15 +618,13 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
#[allow(non_snake_case, dead_code, clippy::too_many_arguments)]
|
||||
#unexpanded
|
||||
}
|
||||
} else if let Ok(mut dummy) = dummy {
|
||||
} else {
|
||||
dummy.sig.ident = unmodified_fn_name_from_fn_name(&dummy.sig.ident);
|
||||
quote! {
|
||||
#[doc(hidden)]
|
||||
#[allow(non_snake_case, dead_code, clippy::too_many_arguments)]
|
||||
#dummy
|
||||
}
|
||||
} else {
|
||||
quote! {}
|
||||
}
|
||||
.into()
|
||||
}
|
||||
@@ -700,10 +703,12 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
#[proc_macro_error::proc_macro_error]
|
||||
#[proc_macro_attribute]
|
||||
pub fn island(_args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
let mut dummy = syn::parse::<DummyModel>(s.clone());
|
||||
let Ok(mut dummy) = syn::parse::<DummyModel>(s.clone()) else {
|
||||
return s;
|
||||
};
|
||||
let parse_result = syn::parse::<component::Model>(s);
|
||||
|
||||
if let (Ok(ref mut unexpanded), Ok(model)) = (&mut dummy, parse_result) {
|
||||
if let (ref mut unexpanded, Ok(model)) = (&mut dummy, parse_result) {
|
||||
let expanded = model.is_island().into_token_stream();
|
||||
if !matches!(unexpanded.vis, Visibility::Public(_)) {
|
||||
unexpanded.vis = Visibility::Public(Pub {
|
||||
@@ -718,15 +723,13 @@ pub fn island(_args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
#[allow(non_snake_case, dead_code, clippy::too_many_arguments)]
|
||||
#unexpanded
|
||||
}
|
||||
} else if let Ok(mut dummy) = dummy {
|
||||
} else {
|
||||
dummy.sig.ident = unmodified_fn_name_from_fn_name(&dummy.sig.ident);
|
||||
quote! {
|
||||
#[doc(hidden)]
|
||||
#[allow(non_snake_case, dead_code, clippy::too_many_arguments)]
|
||||
#dummy
|
||||
}
|
||||
} else {
|
||||
quote! {}
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
@@ -34,8 +34,8 @@ pub fn server_impl(args: TokenStream, s: TokenStream) -> TokenStream {
|
||||
};
|
||||
args.docs = attrs
|
||||
.iter()
|
||||
.cloned()
|
||||
.filter(|attr| attr.meta.path().is_ident("doc"))
|
||||
.cloned()
|
||||
.collect();
|
||||
// default to PascalCase version of function name if no struct name given
|
||||
if args.struct_name.is_none() {
|
||||
|
||||
@@ -26,7 +26,7 @@ bytecheck = { version = "0.7", features = [
|
||||
rustc-hash = "1"
|
||||
serde-wasm-bindgen = "0.5"
|
||||
serde_json = "1"
|
||||
spin-sdk = { git = "https://github.com/fermyon/spin", tag = "v2.0.1", optional = true }
|
||||
spin-sdk = { version = "2", optional = true }
|
||||
base64 = "0.21"
|
||||
thiserror = "1"
|
||||
tokio = { version = "1", features = [
|
||||
|
||||
@@ -156,7 +156,7 @@ use std::any::{Any, TypeId};
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "debug", skip_all,)
|
||||
instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
#[track_caller]
|
||||
pub fn provide_context<T>(value: T)
|
||||
@@ -230,7 +230,7 @@ where
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "debug", skip_all,)
|
||||
instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
pub fn use_context<T>() -> Option<T>
|
||||
where
|
||||
|
||||
@@ -26,7 +26,7 @@ cfg_if::cfg_if! {
|
||||
use std::cell::Cell;
|
||||
|
||||
thread_local! {
|
||||
static IS_SPECIAL_ZONE: Cell<bool> = Cell::new(false);
|
||||
static IS_SPECIAL_ZONE: Cell<bool> = const { Cell::new(false) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,7 +341,7 @@ where
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
name = "Effect::run()",
|
||||
level = "debug",
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
defined_at = %self.defined_at,
|
||||
|
||||
@@ -332,7 +332,7 @@ impl Default for SharedContext {
|
||||
|
||||
#[cfg(feature = "experimental-islands")]
|
||||
thread_local! {
|
||||
pub static NO_HYDRATE: Cell<bool> = Cell::new(true);
|
||||
pub static NO_HYDRATE: Cell<bool> = const { Cell::new(true) };
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-islands")]
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
#![cfg_attr(feature = "nightly", feature(type_name_of_val))]
|
||||
#![cfg_attr(feature = "nightly", feature(auto_traits))]
|
||||
#![cfg_attr(feature = "nightly", feature(negative_impls))]
|
||||
// to prevent warnings on new from popping up when one of those features is stabilized
|
||||
// to prevent warnings from popping up when a nightly feature is stabilized
|
||||
#![allow(stable_features)]
|
||||
|
||||
//! The reactive system for the [Leptos](https://docs.rs/leptos/latest/leptos/) Web framework.
|
||||
|
||||
@@ -537,7 +537,7 @@ where
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
name = "Memo::run()",
|
||||
level = "debug",
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
defined_at = %self.defined_at,
|
||||
|
||||
@@ -86,7 +86,7 @@ use std::{
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature="ssr"),
|
||||
instrument(
|
||||
level = "debug",
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
ty = %std::any::type_name::<T>(),
|
||||
@@ -120,7 +120,7 @@ where
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature="ssr"),
|
||||
instrument(
|
||||
level = "debug",
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
ty = %std::any::type_name::<T>(),
|
||||
@@ -169,7 +169,7 @@ where
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature="ssr"),
|
||||
instrument(
|
||||
level = "debug",
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
ty = %std::any::type_name::<T>(),
|
||||
@@ -295,7 +295,7 @@ where
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature="ssr"),
|
||||
instrument(
|
||||
level = "debug",
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
ty = %std::any::type_name::<T>(),
|
||||
@@ -328,7 +328,7 @@ where
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature="ssr"),
|
||||
instrument(
|
||||
level = "debug",
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
ty = %std::any::type_name::<T>(),
|
||||
@@ -500,7 +500,7 @@ where
|
||||
/// (`value.read()` is equivalent to `value.with(T::clone)`.)
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "debug", skip_all,)
|
||||
instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
#[track_caller]
|
||||
#[deprecated = "You can now use .get() on resources."]
|
||||
@@ -520,7 +520,7 @@ where
|
||||
/// [`Resource::read`].
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "debug", skip_all,)
|
||||
instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
#[track_caller]
|
||||
pub fn map<U>(&self, f: impl FnOnce(&T) -> U) -> Option<U> {
|
||||
@@ -1518,7 +1518,7 @@ impl<S, T> UnserializableResource for ResourceState<S, T> {
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static SUPPRESS_RESOURCE_LOAD: Cell<bool> = Cell::new(false);
|
||||
static SUPPRESS_RESOURCE_LOAD: Cell<bool> = const { Cell::new(false) };
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
|
||||
@@ -951,12 +951,11 @@ impl RuntimeId {
|
||||
false
|
||||
};
|
||||
|
||||
let prev_observer =
|
||||
SetObserverOnDrop(self, runtime.observer.take());
|
||||
let prev_observer = SetObserverOnDrop(runtime.observer.take());
|
||||
|
||||
untracked_result = f();
|
||||
|
||||
runtime.observer.set(prev_observer.1);
|
||||
runtime.observer.set(prev_observer.0);
|
||||
std::mem::forget(prev_observer); // avoid Drop
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -1371,12 +1370,12 @@ impl std::hash::Hash for Runtime {
|
||||
}
|
||||
}
|
||||
|
||||
struct SetObserverOnDrop(RuntimeId, Option<NodeId>);
|
||||
struct SetObserverOnDrop(Option<NodeId>);
|
||||
|
||||
impl Drop for SetObserverOnDrop {
|
||||
fn drop(&mut self) {
|
||||
_ = with_runtime(|rt| {
|
||||
rt.observer.set(self.1);
|
||||
rt.observer.set(self.0);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1393,14 +1392,13 @@ impl Drop for SetObserverOnDrop {
|
||||
)]
|
||||
#[inline(always)]
|
||||
pub fn batch<T>(f: impl FnOnce() -> T) -> T {
|
||||
let runtime_id = Runtime::current();
|
||||
with_runtime(move |runtime| {
|
||||
let batching = SetBatchingOnDrop(runtime_id, runtime.batching.get());
|
||||
let batching = SetBatchingOnDrop(runtime.batching.get());
|
||||
runtime.batching.set(true);
|
||||
|
||||
let val = f();
|
||||
|
||||
runtime.batching.set(batching.1);
|
||||
runtime.batching.set(batching.0);
|
||||
std::mem::forget(batching);
|
||||
|
||||
runtime.run_effects();
|
||||
@@ -1409,12 +1407,12 @@ pub fn batch<T>(f: impl FnOnce() -> T) -> T {
|
||||
.expect("tried to run a batched update in a runtime that has been disposed")
|
||||
}
|
||||
|
||||
struct SetBatchingOnDrop(RuntimeId, bool);
|
||||
struct SetBatchingOnDrop(bool);
|
||||
|
||||
impl Drop for SetBatchingOnDrop {
|
||||
fn drop(&mut self) {
|
||||
_ = with_runtime(|rt| {
|
||||
rt.batching.set(self.1);
|
||||
rt.batching.set(self.0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,7 +242,7 @@ impl<T> StoredValue<T> {
|
||||
with_runtime(|runtime| {
|
||||
let n = {
|
||||
let values = runtime.stored_values.borrow();
|
||||
values.get(self.id).map(Rc::clone)
|
||||
values.get(self.id).cloned()
|
||||
};
|
||||
|
||||
if let Some(n) = n {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_meta"
|
||||
version = "0.5.4"
|
||||
version = "0.5.7"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_router"
|
||||
version = "0.5.4"
|
||||
version = "0.5.7"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
@@ -30,7 +30,7 @@ wasm-bindgen = { version = "0.2" }
|
||||
wasm-bindgen-futures = { version = "0.4" }
|
||||
lru = { version = "0.11", optional = true }
|
||||
serde_json = "1.0.96"
|
||||
itertools = "0.11.0"
|
||||
itertools = "0.12.0"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
|
||||
@@ -59,7 +59,7 @@ where
|
||||
/// different color if it’s a link to the page you’re currently on.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "info", skip_all,)
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
#[component]
|
||||
pub fn A<H>(
|
||||
|
||||
@@ -11,7 +11,7 @@ use web_sys::AnimationEvent;
|
||||
/// that child route is displayed. Renders nothing if there is no nested child.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "info", skip_all,)
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
#[component]
|
||||
pub fn Outlet() -> impl IntoView {
|
||||
|
||||
@@ -14,7 +14,7 @@ use std::{
|
||||
};
|
||||
|
||||
thread_local! {
|
||||
static ROUTE_ID: Cell<usize> = Cell::new(0);
|
||||
static ROUTE_ID: Cell<usize> = const { Cell::new(0) };
|
||||
}
|
||||
|
||||
/// Represents an HTTP method that can be handled by this route.
|
||||
@@ -43,7 +43,7 @@ pub enum Method {
|
||||
/// the element it should display, and data that should be loaded alongside the route.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "info", skip_all,)
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
#[component(transparent)]
|
||||
pub fn Route<E, F, P>(
|
||||
@@ -91,7 +91,7 @@ where
|
||||
/// redirects to `redirect_path` instead of displaying its `view`.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "info", skip_all,)
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
#[component(transparent)]
|
||||
pub fn ProtectedRoute<P, E, F, C>(
|
||||
@@ -150,7 +150,7 @@ where
|
||||
/// the element it should display, and data that should be loaded alongside the route.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "info", skip_all,)
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
#[component(transparent)]
|
||||
pub fn StaticRoute<E, F, P, S>(
|
||||
@@ -198,7 +198,7 @@ where
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "info", skip_all,)
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn define_route(
|
||||
@@ -260,7 +260,7 @@ pub struct RouteContext {
|
||||
impl RouteContext {
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "info", skip_all,)
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
pub(crate) fn new(
|
||||
router: &RouterContext,
|
||||
|
||||
@@ -8,11 +8,17 @@ use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
#[cfg(feature = "transition")]
|
||||
use leptos_reactive::use_transition;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
rc::Rc,
|
||||
sync::atomic::{AtomicUsize, Ordering},
|
||||
};
|
||||
use thiserror::Error;
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
static GLOBAL_ROUTERS_COUNT: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
/// Provides for client-side and server-side routing. This should usually be somewhere near
|
||||
/// the root of the application.
|
||||
#[component]
|
||||
@@ -51,6 +57,7 @@ pub struct RouterContext {
|
||||
pub(crate) inner: Rc<RouterContextInner>,
|
||||
}
|
||||
pub(crate) struct RouterContextInner {
|
||||
id: usize,
|
||||
pub location: Location,
|
||||
pub base: RouteContext,
|
||||
pub possible_routes: RefCell<Option<Vec<Branch>>>,
|
||||
@@ -165,6 +172,7 @@ impl RouterContext {
|
||||
});
|
||||
|
||||
let inner = Rc::new(RouterContextInner {
|
||||
id: GLOBAL_ROUTERS_COUNT.fetch_add(1, Ordering::SeqCst),
|
||||
base_path: base_path.into_owned(),
|
||||
path_stack: store_value(vec![location.pathname.get_untracked()]),
|
||||
location,
|
||||
@@ -203,6 +211,10 @@ impl RouterContext {
|
||||
self.inner.base.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn id(&self) -> usize {
|
||||
self.inner.id
|
||||
}
|
||||
|
||||
/// A list of all possible routes this router can match.
|
||||
pub fn possible_branches(&self) -> Vec<Branch> {
|
||||
self.inner
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::{
|
||||
};
|
||||
use leptos::{leptos_dom::HydrationCtx, *};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cell::{Cell, RefCell},
|
||||
cmp::Reverse,
|
||||
collections::HashMap,
|
||||
@@ -65,7 +66,7 @@ use std::{
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "info", skip_all,)
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
#[component]
|
||||
pub fn Routes(
|
||||
@@ -76,15 +77,16 @@ pub fn Routes(
|
||||
) -> impl IntoView {
|
||||
let router = use_context::<RouterContext>()
|
||||
.expect("<Routes/> component should be nested within a <Router/>.");
|
||||
let router_id = router.id();
|
||||
|
||||
let base_route = router.base();
|
||||
let base = base.unwrap_or_default();
|
||||
|
||||
Branches::initialize(&base, children());
|
||||
Branches::initialize(router_id, &base, children());
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
if let Some(context) = use_context::<crate::PossibleBranchContext>() {
|
||||
Branches::with(&base, |branches| {
|
||||
Branches::with(router_id, &base, |branches| {
|
||||
*context.0.borrow_mut() = branches.to_vec()
|
||||
});
|
||||
}
|
||||
@@ -93,7 +95,8 @@ pub fn Routes(
|
||||
let current_route = next_route;
|
||||
|
||||
let root_equal = Rc::new(Cell::new(true));
|
||||
let route_states = route_states(base, &router, current_route, &root_equal);
|
||||
let route_states =
|
||||
route_states(router_id, base, &router, current_route, &root_equal);
|
||||
provide_context(route_states);
|
||||
|
||||
let id = HydrationCtx::id();
|
||||
@@ -156,15 +159,16 @@ pub fn AnimatedRoutes(
|
||||
) -> impl IntoView {
|
||||
let router = use_context::<RouterContext>()
|
||||
.expect("<Routes/> component should be nested within a <Router/>.");
|
||||
let router_id = router.id();
|
||||
|
||||
let base_route = router.base();
|
||||
let base = base.unwrap_or_default();
|
||||
|
||||
Branches::initialize(&base, children());
|
||||
Branches::initialize(router_id, &base, children());
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
if let Some(context) = use_context::<crate::PossibleBranchContext>() {
|
||||
Branches::with(&base, |branches| {
|
||||
Branches::with(router_id, &base, |branches| {
|
||||
*context.0.borrow_mut() = branches.to_vec()
|
||||
});
|
||||
}
|
||||
@@ -193,11 +197,12 @@ pub fn AnimatedRoutes(
|
||||
let prev_matches = prev
|
||||
.map(|(_, r)| r)
|
||||
.cloned()
|
||||
.map(|location| get_route_matches(&base, location));
|
||||
let matches = get_route_matches(&base, next_route.clone());
|
||||
.map(|location| get_route_matches(router_id, &base, location));
|
||||
let matches =
|
||||
get_route_matches(router_id, &base, next_route.clone());
|
||||
let same_route = prev_matches
|
||||
.and_then(|p| p.first().as_ref().map(|r| r.route.key.clone()))
|
||||
== matches.first().as_ref().map(|r| r.route.key.clone());
|
||||
.and_then(|p| p.first().map(|r| r.route.key.clone()))
|
||||
== matches.first().map(|r| r.route.key.clone());
|
||||
if same_route {
|
||||
(animation_state, next_route)
|
||||
} else {
|
||||
@@ -221,7 +226,8 @@ pub fn AnimatedRoutes(
|
||||
let current_route = create_memo(move |_| animation_and_route.get().1);
|
||||
|
||||
let root_equal = Rc::new(Cell::new(true));
|
||||
let route_states = route_states(base, &router, current_route, &root_equal);
|
||||
let route_states =
|
||||
route_states(router_id, base, &router, current_route, &root_equal);
|
||||
|
||||
let root = root_route(base_route, route_states, root_equal);
|
||||
let node_ref = create_node_ref::<html::Div>();
|
||||
@@ -266,12 +272,13 @@ pub fn AnimatedRoutes(
|
||||
|
||||
pub(crate) struct Branches;
|
||||
|
||||
type BranchesCacheKey = (usize, Cow<'static, str>);
|
||||
thread_local! {
|
||||
static BRANCHES: RefCell<HashMap<String, Vec<Branch>>> = RefCell::new(HashMap::new());
|
||||
static BRANCHES: RefCell<HashMap<BranchesCacheKey, Vec<Branch>>> = RefCell::new(HashMap::new());
|
||||
}
|
||||
|
||||
impl Branches {
|
||||
pub fn initialize(base: &str, children: Fragment) {
|
||||
pub fn initialize(router_id: usize, base: &str, children: Fragment) {
|
||||
BRANCHES.with(|branches| {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
@@ -286,7 +293,7 @@ impl Branches {
|
||||
}
|
||||
|
||||
let mut current = branches.borrow_mut();
|
||||
if !current.contains_key(base) {
|
||||
if !current.contains_key(&(router_id, Cow::from(base))) {
|
||||
let mut branches = Vec::new();
|
||||
let children = children
|
||||
.as_children()
|
||||
@@ -316,15 +323,19 @@ impl Branches {
|
||||
true,
|
||||
base,
|
||||
);
|
||||
current.insert(base.to_string(), branches);
|
||||
current.insert((router_id, Cow::Owned(base.into())), branches);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with<T>(base: &str, cb: impl FnOnce(&[Branch]) -> T) -> T {
|
||||
pub fn with<T>(
|
||||
router_id: usize,
|
||||
base: &str,
|
||||
cb: impl FnOnce(&[Branch]) -> T,
|
||||
) -> T {
|
||||
BRANCHES.with(|branches| {
|
||||
let branches = branches.borrow();
|
||||
let branches = branches.get(base).expect(
|
||||
let branches = branches.get(&(router_id, Cow::from(base))).expect(
|
||||
"Branches::initialize() should be called before \
|
||||
Branches::with()",
|
||||
);
|
||||
@@ -334,14 +345,16 @@ impl Branches {
|
||||
}
|
||||
|
||||
fn route_states(
|
||||
router_id: usize,
|
||||
base: String,
|
||||
router: &RouterContext,
|
||||
current_route: Memo<String>,
|
||||
root_equal: &Rc<Cell<bool>>,
|
||||
) -> Memo<RouterState> {
|
||||
// whenever path changes, update matches
|
||||
let matches =
|
||||
create_memo(move |_| get_route_matches(&base, current_route.get()));
|
||||
let matches = create_memo(move |_| {
|
||||
get_route_matches(router_id, &base, current_route.get())
|
||||
});
|
||||
|
||||
// iterate over the new matches, reusing old routes when they are the same
|
||||
// and replacing them with new routes when they differ
|
||||
@@ -617,7 +630,7 @@ pub(crate) fn create_branch(routes: &[RouteData], index: usize) -> Branch {
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "info", skip_all,)
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
fn create_routes(
|
||||
route_def: &RouteDefinition,
|
||||
|
||||
@@ -37,13 +37,19 @@ pub struct BrowserIntegration {}
|
||||
impl BrowserIntegration {
|
||||
fn current() -> LocationChange {
|
||||
let loc = leptos_dom::helpers::location();
|
||||
let state = window()
|
||||
.history()
|
||||
.and_then(|h| h.state())
|
||||
.ok()
|
||||
.and_then(|s| (!s.is_null()).then_some(s));
|
||||
|
||||
LocationChange {
|
||||
value: loc.pathname().unwrap_or_default()
|
||||
+ loc.search().unwrap_or_default().as_str()
|
||||
+ loc.hash().unwrap_or_default().as_str(),
|
||||
replace: true,
|
||||
scroll: true,
|
||||
state: State(None),
|
||||
state: State(state),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ impl State {
|
||||
pub fn to_js_value(&self) -> JsValue {
|
||||
match &self.0 {
|
||||
Some(v) => v.clone(),
|
||||
None => JsValue::UNDEFINED,
|
||||
None => JsValue::NULL,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,6 +187,8 @@
|
||||
#![cfg_attr(feature = "nightly", feature(auto_traits))]
|
||||
#![cfg_attr(feature = "nightly", feature(negative_impls))]
|
||||
#![cfg_attr(feature = "nightly", feature(type_name_of_val))]
|
||||
// to prevent warnings from popping up when a nightly feature is stabilized
|
||||
#![allow(stable_features)]
|
||||
|
||||
mod animation;
|
||||
mod components;
|
||||
|
||||
@@ -17,6 +17,7 @@ pub(crate) struct RouteMatch {
|
||||
}
|
||||
|
||||
pub(crate) fn get_route_matches(
|
||||
router_id: usize,
|
||||
base: &str,
|
||||
location: String,
|
||||
) -> Rc<Vec<RouteMatch>> {
|
||||
@@ -24,24 +25,31 @@ pub(crate) fn get_route_matches(
|
||||
{
|
||||
use lru::LruCache;
|
||||
use std::{cell::RefCell, num::NonZeroUsize};
|
||||
type RouteMatchCache = LruCache<(usize, String), Rc<Vec<RouteMatch>>>;
|
||||
thread_local! {
|
||||
static ROUTE_MATCH_CACHE: RefCell<LruCache<String, Rc<Vec<RouteMatch>>>> = RefCell::new(LruCache::new(NonZeroUsize::new(32).unwrap()));
|
||||
static ROUTE_MATCH_CACHE: RefCell<RouteMatchCache> = RefCell::new(LruCache::new(NonZeroUsize::new(32).unwrap()));
|
||||
}
|
||||
|
||||
ROUTE_MATCH_CACHE.with(|cache| {
|
||||
let mut cache = cache.borrow_mut();
|
||||
Rc::clone(cache.get_or_insert(location.clone(), || {
|
||||
build_route_matches(base, location)
|
||||
}))
|
||||
Rc::clone(
|
||||
cache.get_or_insert((router_id, location.clone()), || {
|
||||
build_route_matches(router_id, base, location)
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
build_route_matches(base, location)
|
||||
build_route_matches(router_id, base, location)
|
||||
}
|
||||
|
||||
fn build_route_matches(base: &str, location: String) -> Rc<Vec<RouteMatch>> {
|
||||
Rc::new(Branches::with(base, |branches| {
|
||||
fn build_route_matches(
|
||||
router_id: usize,
|
||||
base: &str,
|
||||
location: String,
|
||||
) -> Rc<Vec<RouteMatch>> {
|
||||
Rc::new(Branches::with(router_id, base, |branches| {
|
||||
for branch in branches {
|
||||
if let Some(matches) = branch.matcher(&location) {
|
||||
return matches;
|
||||
|
||||
@@ -1,67 +1,69 @@
|
||||
#![cfg_attr(feature = "nightly", feature(proc_macro_span))]
|
||||
//! This crate contains the default implementation of the #[macro@crate::server] macro without a context from the server. See the [server_fn_macro] crate for more information.
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use server_fn_macro::server_macro_impl;
|
||||
use syn::__private::ToTokens;
|
||||
|
||||
/// Declares that a function is a [server function](https://docs.rs/server_fn/).
|
||||
/// This means that its body will only run on the server, i.e., when the `ssr`
|
||||
/// feature is enabled.
|
||||
///
|
||||
/// You can specify one, two, three, or four arguments to the server function:
|
||||
/// 1. **Required**: A type name that will be used to identify and register the server function
|
||||
/// (e.g., `MyServerFn`).
|
||||
/// 2. *Optional*: A URL prefix at which the function will be mounted when it’s registered
|
||||
/// (e.g., `"/api"`). Defaults to `"/"`.
|
||||
/// 3. *Optional*: The encoding for the server function (`"Url"`, `"Cbor"`, `"GetJson"`, or `"GetCbor`". See **Server Function Encodings** below.)
|
||||
/// 4. *Optional*: A specific endpoint path to be used in the URL. (By default, a unique path will be generated.)
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// // will generate a server function at `/api-prefix/hello`
|
||||
/// #[server(MyServerFnType, "/api-prefix", "Url", "hello")]
|
||||
/// ```
|
||||
///
|
||||
/// The server function itself can take any number of arguments, each of which should be serializable
|
||||
/// and deserializable with `serde`.
|
||||
///
|
||||
/// ```ignore
|
||||
/// # use server_fn::*; use serde::{Serialize, Deserialize};
|
||||
/// # #[derive(Serialize, Deserialize)]
|
||||
/// # pub struct Post { }
|
||||
/// #[server(ReadPosts, "/api")]
|
||||
/// pub async fn read_posts(how_many: u8, query: String) -> Result<Vec<Post>, ServerFnError> {
|
||||
/// // do some work on the server to access the database
|
||||
/// todo!()
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Note the following:
|
||||
/// - **Server functions must be `async`.** Even if the work being done inside the function body
|
||||
/// can run synchronously on the server, from the client’s perspective it involves an asynchronous
|
||||
/// function call.
|
||||
/// - **Server functions must return `Result<T, ServerFnError>`.** Even if the work being done
|
||||
/// inside the function body can’t fail, the processes of serialization/deserialization and the
|
||||
/// network call are fallible.
|
||||
/// - **Return types must implement [Serialize](https://docs.rs/serde/latest/serde/trait.Serialize.html).**
|
||||
/// This should be fairly obvious: we have to serialize arguments to send them to the server, and we
|
||||
/// need to deserialize the result to return it to the client.
|
||||
/// - **Arguments must be implement [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html)
|
||||
/// and [`DeserializeOwned`](https://docs.rs/serde/latest/serde/de/trait.DeserializeOwned.html).**
|
||||
/// They are serialized as an `application/x-www-form-urlencoded`
|
||||
/// form data using [`serde_qs`](https://docs.rs/serde_qs/latest/serde_qs/) or as `application/cbor`
|
||||
/// using [`cbor`](https://docs.rs/cbor/latest/cbor/).
|
||||
#[proc_macro_attribute]
|
||||
pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
match server_macro_impl(
|
||||
args.into(),
|
||||
s.into(),
|
||||
syn::parse_quote!(server_fn::default::DefaultServerFnTraitObj),
|
||||
None,
|
||||
Some(syn::parse_quote!(server_fn)),
|
||||
) {
|
||||
Err(e) => e.to_compile_error().into(),
|
||||
Ok(s) => s.to_token_stream().into(),
|
||||
}
|
||||
}
|
||||
#![cfg_attr(feature = "nightly", feature(proc_macro_span))]
|
||||
//! This crate contains the default implementation of the #[macro@crate::server] macro without a context from the server. See the [server_fn_macro] crate for more information.
|
||||
#![forbid(unsafe_code)]
|
||||
// to prevent warnings from popping up when a nightly feature is stabilized
|
||||
#![allow(stable_features)]
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use server_fn_macro::server_macro_impl;
|
||||
use syn::__private::ToTokens;
|
||||
|
||||
/// Declares that a function is a [server function](https://docs.rs/server_fn/).
|
||||
/// This means that its body will only run on the server, i.e., when the `ssr`
|
||||
/// feature is enabled.
|
||||
///
|
||||
/// You can specify one, two, three, or four arguments to the server function:
|
||||
/// 1. **Required**: A type name that will be used to identify and register the server function
|
||||
/// (e.g., `MyServerFn`).
|
||||
/// 2. *Optional*: A URL prefix at which the function will be mounted when it’s registered
|
||||
/// (e.g., `"/api"`). Defaults to `"/"`.
|
||||
/// 3. *Optional*: The encoding for the server function (`"Url"`, `"Cbor"`, `"GetJson"`, or `"GetCbor`". See **Server Function Encodings** below.)
|
||||
/// 4. *Optional*: A specific endpoint path to be used in the URL. (By default, a unique path will be generated.)
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// // will generate a server function at `/api-prefix/hello`
|
||||
/// #[server(MyServerFnType, "/api-prefix", "Url", "hello")]
|
||||
/// ```
|
||||
///
|
||||
/// The server function itself can take any number of arguments, each of which should be serializable
|
||||
/// and deserializable with `serde`.
|
||||
///
|
||||
/// ```ignore
|
||||
/// # use server_fn::*; use serde::{Serialize, Deserialize};
|
||||
/// # #[derive(Serialize, Deserialize)]
|
||||
/// # pub struct Post { }
|
||||
/// #[server(ReadPosts, "/api")]
|
||||
/// pub async fn read_posts(how_many: u8, query: String) -> Result<Vec<Post>, ServerFnError> {
|
||||
/// // do some work on the server to access the database
|
||||
/// todo!()
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Note the following:
|
||||
/// - **Server functions must be `async`.** Even if the work being done inside the function body
|
||||
/// can run synchronously on the server, from the client’s perspective it involves an asynchronous
|
||||
/// function call.
|
||||
/// - **Server functions must return `Result<T, ServerFnError>`.** Even if the work being done
|
||||
/// inside the function body can’t fail, the processes of serialization/deserialization and the
|
||||
/// network call are fallible.
|
||||
/// - **Return types must implement [Serialize](https://docs.rs/serde/latest/serde/trait.Serialize.html).**
|
||||
/// This should be fairly obvious: we have to serialize arguments to send them to the server, and we
|
||||
/// need to deserialize the result to return it to the client.
|
||||
/// - **Arguments must be implement [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html)
|
||||
/// and [`DeserializeOwned`](https://docs.rs/serde/latest/serde/de/trait.DeserializeOwned.html).**
|
||||
/// They are serialized as an `application/x-www-form-urlencoded`
|
||||
/// form data using [`serde_qs`](https://docs.rs/serde_qs/latest/serde_qs/) or as `application/cbor`
|
||||
/// using [`cbor`](https://docs.rs/cbor/latest/cbor/).
|
||||
#[proc_macro_attribute]
|
||||
pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
match server_macro_impl(
|
||||
args.into(),
|
||||
s.into(),
|
||||
syn::parse_quote!(server_fn::default::DefaultServerFnTraitObj),
|
||||
None,
|
||||
Some(syn::parse_quote!(server_fn)),
|
||||
) {
|
||||
Err(e) => e.to_compile_error().into(),
|
||||
Ok(s) => s.to_token_stream().into(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#![cfg_attr(feature = "nightly", feature(proc_macro_span))]
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(missing_docs)]
|
||||
// to prevent warnings from popping up when a nightly feature is stabilized
|
||||
#![allow(stable_features)]
|
||||
|
||||
//! Implementation of the `server_fn` macro.
|
||||
//!
|
||||
@@ -349,6 +351,7 @@ pub fn server_macro_impl(
|
||||
}
|
||||
|
||||
struct ServerFnName {
|
||||
_attrs: Vec<Attribute>,
|
||||
struct_name: Ident,
|
||||
_comma: Option<Token![,]>,
|
||||
prefix: Option<Literal>,
|
||||
@@ -360,6 +363,7 @@ struct ServerFnName {
|
||||
|
||||
impl Parse for ServerFnName {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let _attrs: Vec<Attribute> = input.call(Attribute::parse_outer)?;
|
||||
let struct_name = input.parse()?;
|
||||
let _comma = input.parse()?;
|
||||
let prefix = input.parse()?;
|
||||
@@ -380,6 +384,7 @@ impl Parse for ServerFnName {
|
||||
let fn_path = input.parse()?;
|
||||
|
||||
Ok(Self {
|
||||
_attrs,
|
||||
struct_name,
|
||||
_comma,
|
||||
prefix,
|
||||
@@ -410,7 +415,7 @@ struct ServerFnBody {
|
||||
/// The custom rusty variant of parsing rsx!
|
||||
impl Parse for ServerFnBody {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
let attrs: Vec<Attribute> = input.call(Attribute::parse_outer)?;
|
||||
let mut attrs: Vec<Attribute> = input.call(Attribute::parse_outer)?;
|
||||
let vis: Visibility = input.parse()?;
|
||||
|
||||
let async_token = input.parse()?;
|
||||
@@ -450,6 +455,12 @@ impl Parse for ServerFnBody {
|
||||
Some((value.unwrap_or_default(), attr.path.span()))
|
||||
})
|
||||
.collect();
|
||||
attrs.retain(|attr| {
|
||||
let Meta::NameValue(attr) = &attr.meta else {
|
||||
return true;
|
||||
};
|
||||
!attr.path.is_ident("doc")
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
vis,
|
||||
|
||||
Reference in New Issue
Block a user