Compare commits

...

30 Commits

Author SHA1 Message Date
Greg Johnston
0c4cf5471d v0.5.7 2024-01-19 13:03:44 -05:00
Greg Johnston
dd0c349554 examples: update axum-session because old version was yanked (#2205) 2024-01-19 12:54:08 -05:00
Greg Johnston
dd5a0ae094 Merge pull request #2203 from leptos-rs/2201
fix: routing regressions caused by `trailing_slash` support
2024-01-19 12:14:20 -05:00
Greg Johnston
5cacb57283 chore: new clippy warnings 2024-01-19 11:14:36 -05:00
Greg Johnston
b356d3cd28 ci: add regression test for #2190 2024-01-19 10:29:30 -05:00
Greg Johnston
ae1de88916 Revert "Better handling for trailing slashes. (#2154) (#2172)"
This reverts commit 1eaf886481.
2024-01-19 10:27:38 -05:00
Greg Johnston
67dd188358 ci: add regression test for matching main page correctly in router example 2024-01-19 10:26:58 -05:00
Joseph Cruz
1d4772251a fix: ci stopped detecting leptos or example changes (#2194) 2024-01-17 18:58:21 -05:00
benwis
98f18e7c31 release: version 0.5.6 2024-01-16 16:57:07 -08:00
Greg Johnston
3f34d9bb23 fix: doc comments breaking server functions (closes #2190) (#2191) 2024-01-16 15:31:29 -05:00
Greg Johnston
44db400f6b chore: fix spin-sdk dependency 2024-01-15 17:11:50 -05:00
Greg Johnston
3a5730800c v0.5.5 2024-01-15 17:10:49 -05:00
Sam Judelson
041b86e6e5 sso_auth_session example (#2117)
* example draft

* fmt

* db delete

* db

* oops clippy

* clippy 2

* - nightly ?

* fmt

* - cargo all features?
2024-01-15 10:48:22 -08:00
Greg Johnston
d71feada7e change: change all tracing levels to trace to reduce verbosity (#2176) 2024-01-13 08:19:12 -08:00
Cody Casterline
1eaf886481 Better handling for trailing slashes. (#2154) (#2172)
* retain trailing slashes in paths but leave matching trail-slash-insensitive

* fix: Allow trailing slashes to remain in leptos_path.

* Better handling for trailing slashes. (#2154)

This adds a trailing_slash option to <Router> and <Route>.

By default, this option is backward compatible with current Leptos
behavior, but users can opt into two new modes for handling trailing
slashes.

* cargo fmt

* Fix redirect routes for wildcard patterns.

* Clippy fixies

* (Re)Reduce the scope of PossibleBranchContext's internals.

* Test real code, not copied code.

* Test TrailingSlash redirects.

* Fixes and more tests for matching "" && "/".

This path is the exception to the rule and *should* be treated
as equivalent regardless of its trailing slash.

* cargo fmt

---------

Co-authored-by: Tadas Dailyda <tadas@dailyda.com>
2024-01-11 13:51:31 -08:00
dependabot[bot]
c7da3998ba chore(deps): bump tj-actions/changed-files in /.github/workflows (#2180)
Bumps [tj-actions/changed-files](https://github.com/tj-actions/changed-files) from 39 to 41.
- [Release notes](https://github.com/tj-actions/changed-files/releases)
- [Changelog](https://github.com/tj-actions/changed-files/blob/main/HISTORY.md)
- [Commits](https://github.com/tj-actions/changed-files/compare/v39...v41)

---
updated-dependencies:
- dependency-name: tj-actions/changed-files
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-11 16:04:21 -05:00
Luxalpa
10bbeea697 fix: remember router state (fixes #2164) (#2165) 2024-01-10 19:51:37 -05:00
webmstk
2d70229608 chore: add rust-toolchain.toml to examples (closes #2151) (#2161) 2024-01-07 15:29:54 -05:00
Paul Hansen
bbcef811f4 chore: remove dead code in actix tailwind example (#2160) 2024-01-07 15:29:15 -05:00
Simon Bihel
f5bf539148 fix: allow multiple SSRed Leptos apps on same server (closes #1806) (#2135) 2024-01-07 15:28:50 -05:00
Greg Johnston
855a3c65c0 chore: remove newly-detected unused tuple fields (#2169) 2024-01-06 18:25:20 -05:00
SleeplessOne1917
16cf3c4eaf feat: make IntoAttribute more flexible for Option<T: IntoAttribute> (#2130) 2023-12-30 13:10:07 -05:00
Greg Johnston
da533ad4e0 chore: clarify cargo-make and examples (see #2141) (#2150) 2023-12-30 08:14:23 -05:00
JackSpagnoli
b4a9db51be chore: fix broken tailwind example repo link (#2138) 2023-12-28 20:07:49 -05:00
Greg Johnston
15946c6136 fix: emit original token stream when there are syntax errors in #[component] or #[island] function signature (closes #2133) (#2134) 2023-12-25 14:30:31 -05:00
Greg Johnston
9f4c480725 docs: specify form for component generics (#2129) 2023-12-23 07:36:28 -05:00
Greg Johnston
74f9c3cd82 fix: <Transition/> fallback should show if client-rendered (closes #2123) (#2128) 2023-12-22 17:10:09 -05:00
martin frances
34fb39c976 chore: fix lifetime on example (#2125)
```bash
cd leptos/leptos_macro/example
cargo clippy
```

warning: the following explicit lifetimes could be elided: 'a
  --> src/lib.rs:41:20
   |
41 | fn TestMutCallback<'a, F>(mut callback: F, value: &'a str) -> impl IntoView
   |                    ^^                              ^^
   |
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_lifetimes
   = note: `#[warn(clippy::needless_lifetimes)]` on by default
help: elide the lifetimes
   |
41 - fn TestMutCallback<'a, F>(mut callback: F, value: &'a str) -> impl IntoView
41 + fn TestMutCallback<F>(mut callback: F, value: &str) -> impl IntoView
   |

warning: `example` (lib) generated 1 warning (run `cargo clippy --fix --lib -p example` to apply 1 suggestion)
2023-12-20 19:10:59 -05:00
martin frances
ea80a21a54 chore: remove duplicate itertools versions (#2124)
The root cause is the family of leptos modules requiring both versions 0.10.5 and 0.11.0
This PR will fix that. ( Also needs a bump to 0.12.0 )

```
warning: multiple versions for dependency `itertools`: 0.10.5, 0.11.0
  |
  = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#multiple_crate_versions
note: the lint level is defined here
 --> src/lib.rs:4:9
  |
4 | #![warn(clippy::cargo)]
  |         ^^^^^^^^^^^^^
  = note: `#[warn(clippy::multiple_crate_versions)]` implied by `#[warn(clippy::cargo)]`
```
2023-12-20 19:10:00 -05:00
Greg Johnston
33590d487b chore: clean up warnings causing CI issues (#2119) 2023-12-18 08:15:46 -05:00
100 changed files with 1515 additions and 350 deletions

View File

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

View File

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

View File

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

@@ -10,4 +10,5 @@ Cargo.lock
.direnv
.envrc
.vscode
.vscode
vendor

View File

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

View File

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

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View File

@@ -1,3 +1,2 @@
[toolchain]
channel = "nightly"

View File

@@ -1,3 +1,2 @@
[toolchain]
channel = "nightly"

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View File

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

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View File

@@ -1,3 +1,2 @@
[toolchain]
channel = "nightly"

View File

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

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View 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

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

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

View 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
View 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
}

View 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" ];
})
];
};
});
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

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

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

View 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),
)),
}
}
}
}

View 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/> }
});
}
}
}

View 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.
}
}
}

View 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>
}
}

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

View File

View File

@@ -0,0 +1,7 @@
.pending {
color: purple;
}
a {
color: black;
}

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View File

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

View File

@@ -1,3 +1,2 @@
[toolchain]
channel = "nightly"

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View File

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

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_meta"
version = "0.5.4"
version = "0.5.7"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"

View File

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

View File

@@ -59,7 +59,7 @@ where
/// different color if its a link to the page youre 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>(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 its 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 clients perspective it involves an asynchronous
/// function call.
/// - **Server functions must return `Result<T, ServerFnError>`.** Even if the work being done
/// inside the function body cant 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 its 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 clients perspective it involves an asynchronous
/// function call.
/// - **Server functions must return `Result<T, ServerFnError>`.** Even if the work being done
/// inside the function body cant 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(),
}
}

View File

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